二十、自定义类型:结构体
欢迎阅读这篇文章 目录1、结构体类型的声明1.1结构的声明1.1.1结构体变量的创建和初始化1.2结构的特殊声明1.3结构的自引用2、结构体内存对齐2.1对齐规则2.1.1练习2.2为什么存在内存对⻬?2.3修改默认对齐数3、结构体传参4、结构体实现位段4.1什么是位段4.2位段的内存分配4.3位段的跨平台问题4.4位段使用的注意事项1、结构体类型的声明结构体是一些值的集合这些值称为成员变量。结构的每个成员可以是不同类型的变量。1.1结构的声明structtag{member-list;}variable-list;例如描述一个学生//描述一个学生structstu{charname[20];//姓名intage;//年龄charsex[5];//性别};1.1.1结构体变量的创建和初始化#includestdio.h//描述一个学生structstu{charname[20];//姓名intage;//年龄charsex[5];//性别};intmain(){//按照结构体成员的顺序初始化structstus1{zhangsan,18,男};printf(%s\n,s1.name);printf(%d\n,s1.age);printf(%s\n,s1.sex);//按照指定的顺序初始化structstus2{.age10,.namelisi,.sex男};printf(%s\n,s2.name);printf(%d\n,s2.age);printf(%s\n,s2.sex);return0;}1.2结构的特殊声明在声明结构的时候可以不完全声明。//匿名结构体类型struct{inta;charb;floatc;}x;struct{inta;charb;floatc;}*p;上方的两个结构在声明的时候省略掉了结构体标签。思考下方代码是否成立px;警告编译器会把上⾯的两个声明当成完全不同的两个类型所以是⾮法的。总结匿名的结构体类型如果没有对结构体类型重命名的话基本上只能使⽤⼀次。匿名结构体的类型定义只在当前这一条声明语句中有效语句结束后就再也无法引用这个类型了。1.3结构的自引用在结构中是否可以包含一个类型为该结构体本身的成员比如定义一个链表节点structnode{intdate;structnodenext;};上述代码正确吗想一下如果正确那么sizeof(struct node)应该是多少仔细分析其实是不⾏的因为⼀个结构体中再包含⼀个同类型的结构体变量这样结构体变量的⼤⼩就会⽆穷的⼤是不合理的。我们可以在定义一个结构体的时候一部分放数据另一部分放下一个结构体的地址这样就是正确的自引用。structnode{intdate;structnode*next;};在结构体⾃引⽤使⽤的过程中夹杂了typedef对结构体类型重命名也容易引⼊问题看看下⾯的代码可⾏吗typedefstructnode{intdate;node*next;}node;这段代码不可行。因为node是typedef定义的别名要等整个结构体定义完成后才会生效但在结构体内部写node* next时别名node还未被声明编译器无法识别这个类型因此会报错。而struct node是结构体标签从声明开始就已经生效所以结构体自引用时必须用struct node* next而不能直接用别名node* next。2、结构体内存对齐我们已经掌握了结构体的使用下面来探讨一下结构体的大小它涉及了结构体内存对齐2.1对齐规则规则结构体的第1个成员对齐到和结构体变量起始位置偏移量为0的地址处。从第2个成员变量开始都要对齐到偏移量为自身对⻬数的整数倍的地址处。对齐数编译器默认的一个对齐数与该成员变量大小的较小值。VS 中默认的值为 8Linux中 gcc 没有默认对⻬数对⻬数就是成员⾃⾝的⼤⼩结构体总大小为最大对齐数结构体中每个成员变量都有⼀个对⻬数所有对⻬数中最⼤的的整数倍。如果嵌套了结构体嵌套的结构体成员对齐到自己的成员中最⼤对⻬数的整数倍处结构体的整体⼤⼩就是所有最⼤对⻬数含嵌套结构体中成员的对⻬数的整数倍。2.1.1练习//练习1intmain(){structS1{charc1;//大小1 默认对齐数8 --对齐数1inti;//大小4 默认对齐数8 --对齐数4charc2;//大小1 默认对齐数8 --对齐数1};printf(%zu\n,sizeof(structS1));}第1个成员c1放在和结构体变量起始位置偏移量为0的地址处。第2个成员是int类型的大小是4默认对齐数是8对齐数取4应放在偏移量为4的倍数4的位置处。第3个成员是char类型的大小是1默认对齐数是8对齐数取1应放在偏移量为1的倍数8的位置处。要求结构的大小是最大的对齐数的倍数所以还需要浪费3个空间最终大小是12。//练习2intmain(){structS2{charc1;//大小是1默认对齐数是8对齐数为1charc2;//大小是1默认对齐数是8对齐数为1inti;//大小是4默认对齐数是8对齐数为4};printf(%zu\n,sizeof(structS2));}第1个成员c1放在和结构体变量起始位置偏移量为0的地址处。第2个成员是char类型的大小是1默认对齐数是8对齐数取1应放在偏移量为1的倍数2的位置处。第3个成员是int类型的大小是4默认对齐数是8对齐数取4应放在偏移量为4的倍数4的位置处。要求结构的大小是最大的对齐数的倍数所以最终大小是8。//练习3intmain(){structS3{doubled;//大小是8默认对齐数是8对齐数是8charc;//大小是1默认对齐数是8对齐数是1inti;//大小是4默认对齐数是8对齐数是4};printf(%zu\n,sizeof(structS3));}第1个成员d放在和结构体变量起始位置偏移量为0的地址处。第2个成员是char类型的大小是1默认对齐数是8对齐数取1应放在偏移量为1的倍数8的位置处。第3个成员是int类型的大小是4默认对齐数是8对齐数取4应放在偏移量为4的倍数12的位置处。要求结构的大小是最大的对齐数的倍数所以最终大小是16。//练习4-结构体嵌套问题structS3{doubled;//大小是8默认对齐数是8对齐数是8charc;//大小是1默认对齐数是8对齐数是1inti;//大小是4默认对齐数是8对齐数是4};//大小是16intmain(){structS4{charc1;//大小是1默认对齐数是8对齐数是1structS3s3;//大小是12默认对齐数是8对齐数是8doubled;//大小是8默认对齐数是8对齐数是8};printf(%zu\n,sizeof(structS4));}第1个成员c1放在和结构体变量起始位置偏移量为0的地址处。第2个成员是struct S3类型的大小是16默认对齐数是8对齐数取8应放在偏移量为8的倍数8的位置处。第3个成员是double类型的大小是8默认对齐数是8对齐数取8应放在偏移量为8的倍数24的位置处。要求结构的大小是最大的对齐数8的倍数所以最终大小是32。2.2为什么存在内存对⻬?1.平台原因 (移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常。2. 性能原因数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于为了访问未对⻬的内存处理器需要作两次内存访问⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数那么就可以⽤⼀个内存操作来读或者写值了。否则我们可能需要执⾏两次内存访问因为对象可能被分放在两个8字节内存块中。总体来说结构体的内存对⻬是拿空间来换取时间的做法。那在设计结构体的时候我们既要满⾜对⻬⼜要节省空间就要做到让占⽤空间⼩的成员尽量集中在⼀起。2.3修改默认对齐数#pragma这个预处理指令可以改变编译器的默认对⻬数。#includestdio.h#pragmapack(1)//设置默认对齐数是1structs5{charc1;charc2;intr;};#pragmapack()//恢复默认对齐数intmain(){printf(%zu,sizeof(structs5));}结构体在对⻬⽅式不合适的时候我们可以⾃⼰更改默认对⻬数。3、结构体传参下面看两个代码//结构体传参#includestdio.hstructstu{intdate[10];intnum;};voidprint1(structstus){inti0;for(i;i5;i){printf(%d ,s.date[i]);}printf(\n);printf(%d\n,s.num);}voidprint2(structstu*s){inti0;for(i;i5;i){printf(%d ,s-date[i]);}printf(\n);printf(%d\n,s-num);}intmain(){structstus{{1,2,3,4,5},100};print1(s);print2(s);return0;}上面的两个打印的函数应该选print1还是print2⾸选print2函数。原因函数传参的时候参数是需要压栈会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候结构体过⼤参数压栈的的系统开销⽐较⼤所以会导致性能的下降。结论结构体传参的时候要传结构体的地址。4、结构体实现位段4.1什么是位段位段的声明和结构有两处不同位段的成员只能是int、unsigned int、signed int、char。位段成员的后面要有一个冒号和一个数字。例如//位段的声明structa{int_a:3;int_b:4;int_c:6;int_d:5;};4.2位段的内存分配位段的空间上是按照需要以4个字节int或者1个字节char的⽅式来开辟的。位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使⽤位段。一个例子演示内存是如何分配的//⼀个例⼦structS{char_a:3;char_b:4;char_c:5;char_d:4;};intmain(){structSs{0};s._a10;s._b12;s._c3;s._d4;return0;}下方的演示是基于vs2026进行的struct开辟内存的过程首先a是char类型的按照1个字节开辟空间8个比特位_a3从开辟的内存高地址开始向低地址读取3个比特位给_a再读取4个比特位给_b_c需要5位剩下的不够了再开辟1个字节的空间再从开辟的内存高地址开始向低地址读取5个比特位给_c_d需要4位剩下的不够了再开辟1个字节的空间再从开辟的内存高地址开始向低地址读取4个比特位给_d最终如上图所示。下面进行初始化的过程先将所有的比特位初始化为0。_a10(01010)只能从低位开始取3位存进_a中_b12(01100)只能从低位开始取4位存进_b中_c3(00011)只能从低位开始取5位存进_c中_d4(00100)只能从低位开始取4位存进_d中最终如下将它转为16进制后是也就是0x62、0x03、0x044个比特位写成1位16进制位段的地址就是 62 03 04 cc小端字节序存储通过监视可以验证4.3位段的跨平台问题int位段被当成有符号数还是无符号数是不确定的。位段中最大位的数目不能确定16位机器最大1632位机器最大32写成27在16位机器上会出问题。位段中成员在内存中是从左向右分配还是从右向左分配是标准为定义的。当⼀个结构包含两个位段第⼆个位段成员⽐较⼤⽆法容纳于第⼀个位段剩余的位时是舍弃剩余的位还是利⽤这是不确定的。总结跟结构相⽐位段可以达到同样的效果并且可以很好的节省空间但是有跨平台的问题存在。4.4位段使用的注意事项有时候位段的几个成员会共用1个字节这样有些成员的起始位置并不是某个字节的起始位置。我们知道内存中每个字节才会分配一个地址一个字节内部的bit位是没有地址的。位段的成员是没有地址的。所以不能对位段的成员使⽤操作符这样就不能使⽤scanf直接给位段的成员输⼊值只能是先输⼊放在⼀个变量中然后赋值给位段的成员。//位段不能使用scanf#includestdio.hstructstu{int_a:3;int_b:5;int_c:6;};intmain(){structstus{0};//scanf(%d, s._b);//错误intd0;scanf(%d,d);s._bd;//正确return0;}