结构体
前言
记录对于结构体知识的了解
结构体?
结构体是一种复杂类型对象,可以用来描述一些复杂对象;
比如说:对于一个人来说,他不光只有年龄、身高、体重、姓名等,我们通过基本数据类型无法来描述它,为此我们得想办法,解决他啊!C语言为我们提供了struct关键字来解决这个问题,利用struct关键字我们可以用来定义结构体,把这些原本单个的数据给它打包在一起,形成一个新的类型;
那么struct怎么用呢?
举个例子,比如说我们要描述人:
structPerson{char name[20];char sex[5];int age;}
我们想这样定义就好了,其中struct Person就是和int 、char、double同等地位的东西了,struct Person也是一个数据类型,只不过它是复杂数据类型,由基本数据类型组成,能描述的信息也就更多。这也是C语言的灵魂之一;
基本语法就是:
struct +类型名
{
成员变量;
};//主义这里要加分号结尾;
因此我们有了这样的一个类型,我们该如何定义变量呢?
定义变量
上面我们说了struct定义出来的复杂数据类型和基本数据类型是一样的地位,都是类型,那么自然的定义变量的方式也是一样的;
基本数据类型是怎么定义的我们就怎么定义:
红色部分是我们的类型,黑色部分是变量;
如何赋初值?
赋初值就和基本数据类型有所区别了,基本数据类型,只有一个,我们只需要=进行赋初值就行了;但是复杂数据类型不一样啊,它里面有多个基本数据类型,为了能给每个都赋值到位,我们应该用{ }的形式进行赋值操作,其中格式按照基本数据类型对应的格式就好了:
比如:
structPerson p1={"张三","man",19};
必须按照基本数据类型的顺序正确赋值,不能随意颠倒顺序;!!!
就像数组那样一样赋值就像了;
我们还有一直赋初值的方式:
上面一种方式不是要求我们不能颠倒赋值顺序嘛,这一种方式就是破解上一个方式的:
structPerson p1={.age=19,.sex="man",.name="张三"};
像这样的赋值方式相当于我们“指哪打哪”想给那个成员变量赋值,就给那个成员变量赋值;这里我们讲到了**.**(点)这个操作符,也就是访问结构体的方式;
结构体的访问
既然我们定义好了结构体同时也对结构体进行了相应的初始化,那么我们应该如何去访问结构体里面的数据呢?
C语言为我们提供了两个操作数去解决这个问题:
.操作符和->操作符;
.操作符是配合着普通结构体变量来使用的;
->操作符是配合结构体指针来使用的;
具体怎么使用呢,我们来具体看一看:
1、.操作符使用
2、->操作符使用
结构体的嵌套使用
当然我们的结构体不是只能由基本数据类型组成,还可以由结构体组成:
比如说:
注意事项
1、结构体并不一定要有名字,匿名结构体(没有名字的结构体也是可以的),但是该结构只能用一次;什么意思呢?
我们来看代码;
我们不能单独将匿名结构体拿出来创建变量,我们只能在定义结构体的的时候随便创建一个变量(类似于左边图),这就是为什么匿名结构体只能使用一次?
那么如果现在有我定义了两个一模一样的匿名结构体,编译器会认为这两个结构体是同一个数据类型吗?
我们来验证一下:
测试代码:
struct{char name[20];char sex[5];char id[20];int age;}str1 ={"张三","man","110112119112",10};struct{char name[20];char sex[5];char id[20];int age;}*str2;intmain(){
str2 =&str1;return0;}
我们可以看到虽然编译器让段代码通过了,但是还是保了个警告;
我们知道“=”两边的数据类型必须一样,现在报了警告,这说明现在“=”两边的数据类型出现了不统一,这也就说明了虽然两个匿名结构体虽然成员变量完全一模一样,但是在编译器看来他们仍然是两个不同的数据类型;当然对于两个完全相同的有名结构来说就不会存在这样的情况,编译器会直接给你报错误,说你重定义了这个数据类型:
2、对于结构体来说,它的类型名字一般都比较长,我们有没有什么办法使它的名字变得短一点;当然可以,我们可以利用typedef关键字对类型进行重名名,比如说:
我们利用了typedef使得原来冗长的数据类型变得简单起来了,虽然简单起来了,我们确不能滥用,因为我们利用typedef过后,代码可读性变差了,就比如:Person,单单放在这里,我们可以说他联合体类型、也可以说他是枚举类型同时也可以说他是结构体类型,给人的可读性不是太友好;当然这只是一方面的原因,同时一些公式也会对此做一些代码规范的要求,有些公式可能为了代码的可读性,禁止使用typedef,当然有的公司则无此限制;具体怎么使用看个人情况;
3、我们定义一个结构体最好定义在函数的外面,不要再函数内部定义,这样的话我们才能进行更多的操作和减少不必要的麻烦;
4、结构体虽然能嵌套使用,但是不能嵌套自己;
比如这样:
为什么呢?
你看我们好像无法计算出struct Person这个结构体的大小对吧,我们知道name是20个字节,那struct Person是多大呢?你看我们是不是又回到了原点,似乎该计算过程在无限递归下去,我们无法求的一个准确的大小,编译器也就无法为其开辟一个合理的空间;
但是呢我们可以嵌套自己类型的指针啊,你想啊指针的大小是固定的(4/8)我们可以明确算出结构体的大小啊!
事实的确是这样的!
结构体的大小
既然上面我们谈到了结构体的大小问题,那我们便来看看一个结构体,
并算一算结构体的大小是多少;
structPerson{char a;char b;int c;};
我们来算一算这个结构体的大小
可能我们第一次算的时候会算成6;那么结果是不是呢,我们运行一下:
我们可以看到运行结果是8,这是为什么??
这主要是由于啊,结构体的大小并不是简单的相加就解决的,对于结构体来说,它们存在这内存对齐这一说法;
何为内存对齐?
内存对齐
内存对齐:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
下面我们就来具体应用一下该规则(VS2019环境,默认对齐数:8):
下面我们来放空间:
这也就是为什么该结构体大小是8的原因;
既然明白了原理,我们立刻趁热打铁,继续算几个:
structS2{char c1;int c2
char c3;};structS3{double d;char c;int i;};
我们画一个s3吧,另一个当作练习:
空间分布:
运行结果:
的确如此;
那么我们现在来看一个结构体嵌套的结构体的大小:
structS3{double d;char c;int i;};structS4{char a;int b;structS3 e;};
由上面可知S3的总大小是16个字节,然后呢根据内存对齐的第4条规则,我们可以得出S3结构体的对齐数为其成员对齐数的最大值,也就是:8,因此S3的对齐数为8;
接下来我们算一下对齐数表:
接下来分配内存:
运行一下:
的确如此!!!
默认对齐数的修改
既然由默认对齐数这个东西,我们能不能去修改掉它呢?
当然可以;
我们可以通过预处理指令来修改
比如#pragma pack(4)//修改默认对齐数为4
#pragma pack()//取消默认对齐数修改
下面我们还是来计算
structS3{double d;char c;int i;};
我们快速计算一下:
07//d占用11//浪费
8//c占用
9
1215//i占用7//d从偏移量为0占用
总的:16字节,最大对齐数是4,16是4的整数倍,故最后结构体总大小为:16字节;
运行截图:
同时我们可以利用offsetof宏函数来计算一下每个成员变量的偏移量,同时验证我们的推理过程是否正确;
0
8//c从偏移量为8占用
911//浪费15//i从偏移量为12占用
12
在使用前先了解一下offsetof宏函数:
包含在同文件<stddef.h>里面;
运行结果:
的确如此;
为什么存在内存对齐?
从两个方面来说:
1、平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
比如说:32位平台,一次性只能读取四个字节的数据;
structS{char a;int i;};
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
结构体传参
structS{int data[1000];int num;};structS s ={{1,2,3,4},1000};//结构体传参voidprint1(structS s){printf("%d\n", s.num);}//结构体地址传参voidprint2(structS* ps){printf("%d\n", ps->num);}intmain(){print1(s);//传结构体print2(&s);//传地址return0;}
上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:
结构体传参的时候,首选传结构体的地址。
位段
结构体的基础知识讲完了,我们就来谈谈结构的位段;
什么是位段?
1、位段与结构体的声明是类似的,但是位段的成员必须是int、unsigned int、signedint、char;
2、位段的成员名后面有一个“ : ”和一个数字,以" ; "结尾;
3、访问的方式与结构体一样(可以通过点操作符和箭头操作符);
4、位段不存在内存对齐;
具体实例:
structA{int _a:2;int _b:5;int _c:10;int _d:30;};
首先我们来解析一下,这个位段的位是什么意思,以及" : "后面的数字代表什么意思?
首先,位嘛,就是比特位的那个位,而冒号后面的数字,就表示这个变量要占用几个比特位;比如上面结构体中,int _a:2;就表示成员变量_a只需要使用2个比特位就够了,不在需要一个int的大小;
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。 4、不建议int和char混用
在讨论位段的内存分配之前,我们先来算一个位段的大小:
structS{char a:3;char b:4;char c:5;char d:4;};
我们可以看到该结构只占3个字节,其实按照我们之前的理论我们可以得出相应的结论;
是char类型的位段;
编译器一上来先开辟1个字节,给你用,用完了无,我在给你开辟;
然后a说我要3个比特位,还剩下5个比特位;
b说我要4个比特位,还剩下1个比特位;
c说我要5个比特位,这时候只剩下一个1特位,不够啊,那我再给你开辟一个字节;嗯,现在空间够了,但是我们现在面临一个选择的问题,就是上一次剩下的那个字节,我到底是用还是不用?这个问题C语言并没有做出明确规定,是由编译器决定的,这是位段可移植性差的一个点,再VS2019中,编译器是不用的(博主用的VS2019),那我们现在假设不用那么我在新开的一个字节中用了5个比特位,还剩下3比特位;
d说,我要4比特位,不够啊,编译器在开一个字节,这时就够了,总体来说,编译器一共就开辟了3个字节,因此,我们sizeof算出来的就是3字节;
深入剖析位段“存”数据
我们刚才讨论了位段的空间大小,现在让我们更进一步去研究一下,位段是怎么存数据的;
还是以上述位段为例:
测试代码:
structS{char a:3;char b:4;char c:5;char d:4;};structS s ={0};
s.a =10;
s.b =12;
s.c =3;
s.d =4;
首先,我们直到S会占3字节,我们把这3个字节全部用0初始化:
则这3个字节的每个位里面放的都是0;
现在,我们将10的放进去:
10
补码:00000000000000000000000000001010
我们只存3位进去,也就是只有后3位,010,
但是现在我们面临一个问题选择问题?
我们是从左边还是右边来使用这一个字节?这是不确定的,C语言没有明确规定,完全取决于编译器,因此这是位段可移植性差的一个点;
我们假设是从右边开始存的,那么该字节内容就变为了:
然后我们再来看b,b=12
12:
补码:
00000000000000000000000000001100,我们只存4位进去,也就是(1100);更新一下第一字节里面的内容;
现在s.c=3;
3的补码:00000000000000000000000000000011
我们要存5个比特位进去(00011),现在不够了,我们在开辟一个字节,
但是现在我们又面临一个选择的问题,我们用绿色线圈起来的那个位,我们到底用不用?还是直接从新的字节开始?对于这样的问题C语言没有做出明确规定,用还是不用这个问题却决于编译器,这也是位段可移植性差的又一个原因;现在我们接着假设,这个比特位咱们不用,那么我们新的内容就是:
接着s.d=4;
4的补码:00000000000000000000000000000100
我们只存4比特位进去(0100),不够啊,我们再开一个字节:
我们来算一下这3个字节对应的16进制:62 03 04
我们看到的就应该是这样的,我们来验证一下:
我们可以看到的确是这样,那说明我们之前的假设刚好假设正确;事实上这是VS2019对于位段的处理方法;
位段的“取”
既然我们利用位段存了数据,我们得取出来啊?不然我们存进去了,取不出来,有啥用啊;
根据我们刚才存进去的样子:
我们以%d打印s.a那么a实际存的是010,最高位为0,我们会发生整型提升,提升成int类型:
既printf实际打印的是
00000000000000000000000000000010
原码就是2;
我们再来看b,b里面实际存的是1100,%d打印发生整型提升:
11111111111111111111111111111100%d打印,符号位为1,为负数
1000000000000000000000000011
1000000000000000000000000100
也就是-4;
如果是%u打印,就是直接打印:11111111111111111111111111111100
也就是一个很大的数:
位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。 (对于一个int位段的的位段,我们一次性会开辟4字节,但是这4字节的最高位会不会北当成符号位,是不确定的,是由编译器决定的)
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。 比如:
structS{int a:2;int b:20;int C:30;}
在32位平台下,int占4个字节,但是在早期16位平台下int占2个字节,我们如果要给b分给20比特位,在32位平台下没问题,但是在16位平台下就跑不过去了;
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
位段的应用
位段的最大优点就在于可以最大限度的节省空间,就比如,对于一些数据来说,我们只需要4比特位来存储就够了,就没必要开辟一个int来存,这样大大节省了空间!!!
版权归原作者 南猿北者 所有, 如有侵权,请联系我们删除。