前言说到结构体我们首先来回忆一下之前学的数组数组的作用是来存储一批类型相同的数据比较intfloat等但我们发现数组只能存储类型相同的数据有时候我们需要存储不同类型的数据比如存储一个学生需要用到的类型有年龄姓名、学号、成绩等C语言为了解决这个问题增加了结构体这种自定义的数据类型让程序员可以自己创造适合的类型。结构是一些值的集合这些值称为成员变量。结构的每个成员可以是不同类型的变量如标量、数组、指针甚至是其他结构体。目录前言1.结构体类型和结构体变量1.1结构体声明1.2结构体特殊声明1.3 结构体自引用1.4 结构体变量的定义和初始化1.5 结构体成员访问操作符2.结构体内存对齐2.1对齐规则2.2为什么存在内存对齐2.3修改默认对齐数3.结构体传参4.结构体实现位段4.1什么是位段4.2位段的内存分配4.3位段的跨平台问题4.4位段的应用4.5位段使用的注意事项1.结构体类型和结构体变量1.1结构体声明structtag//标签{member-list;//结构体成员有1个或多个}variable-list;//可有可无分号(;)必须有struct是用于定义具体结构体类型的关键字tag是结构体标签member-list结构体成员有1个或多个variable-list结构体变量名可有可无分号(;)必须有例描述一个学生structStu{charname[20];//名字intage;//年龄charsex[5];//性别charid[20];//学号};//分号不能丢要注意的是结构体类型名如上例中的Stu是定义的类型名而不是变量名就好像整型的类型名为int双精度实型的类型名为double字符型的类型名为char一样只不过整型、双精度实型、字符型等基本数据类型是C编译系统已经定义的用户可以直接用它们来定义相应类型的变量而结构体类型是用户根据数据处理的需要临时定义的一种类型一经定义就可以像系统定义的类型一样使用了。但系统并不为类型分配存储空间为了能在程序中使用结构体类型的数据必须定义结构体类型的变量。1.2结构体特殊声明在声明结构的时候可以不完全的声明。//匿名结构体类型只能使用一次后续不能使用这个类型创建变量//(假设后续你要创建但你不知道类型名所以不能使用这个类型创建变量struct{inta;charb;floatc;}x;struct{inta;charb;floatc;}a[20],*p;//a[20]结构体数组变量p存放的是结构体的地址上面的两个结构在声明的时候省略掉了结构体标签tag。那么问题来了//在上⾯代码的基础上下⾯的代码合法吗px;警告编译器会把上面的两个声明当成完全不同的两个类型所以是非法的。p的类型是struct 匿名的第二个结构体 *x的类型是struct 匿名的第一个结构体 *二者不能隐式转换也不能直接赋值。匿名的结构体类型如果没有对结构体类型重命名的话基本上只能使用⼀次。1.3 结构体自引用在结构中包含⼀个类型为该结构本身的成员是否可以呢比如定义⼀个链表的节点structNode{intdata;structNodenext;//结构体内创建结构体无限死循环};上述代码正确吗如果正确那sizeof(struct Node)是多少仔细分析其实是不行的因为⼀个结构体中再包含⼀个同类型的结构体变量这样结构体变量的大小就会无穷的大是不合理的。正确的自引用方式structNode{intdata;structNode*next;};在结构体自引用使用的过程中夹杂了typedef对匿名结构体类型重命名也容易引入问题看看下面的代码可行吗typedefstruct{intdata;Node*next;}Node;答案是不行的原因在于typedef定义的新类型名Node在结构体定义结束之后才生效而你在结构体内部已经试图使用Node* next;此时编译器还不认识Node。注如果在 C 代码上面的错误写法反而是合法的因为 C 中类名在类体内会立即生效。但在 C 语言中必须遵守上述规则。解决方案如下定义结构体不要使用匿名结构体了typedefstructNode{intdata;structNode*next;}Node;1.4 结构体变量的定义和初始化结构体变量的定义(1) 先声明结构体类型再定义变量名// 声明结构体类型带有标签 studentstructstudent{charname[20];intage;floatscore;};// 之后使用 struct student 定义变量structstudentstu1;(2) 在声明类型的同时定义变量// 声明结构体类型带有标签 studentstructstudent{charname[20];intage;floatscore;}stu1stu2*p;(3) 不指定类型名而直接定义结构体类型变量// 声明结构体类型带有标签 studentstruct{charname[20];intage;floatscore;}stu1stu2;结构体变量的初始化(1)直接初始化structstudentstu1{“彭于晏”,20,89.5f};(2)指定成员初始化C99标准structstudentstu1{.age22,.name彭于晏,.score89.5f};1.5 结构体成员访问操作符结构体成员的直接访问结构体成员的直接访问是通过点操作符.访问的。点操作符接受两个操作数。如下所示#includestdio.hstructPoint{intx;inty;}p{1,2};//结构体变量并赋值为12intmain(){printf(x:%d y:%d\n,p.x,p.y);//通过点.访问return0;}使用方式结构体变量名.成员名结构体成员的间接访问有时候我们得到的不是⼀个结构体变量⽽是得到了⼀个指向结构体的指针。如下所示#includestdio.hstructPoint{intx;inty;};intmain(){structPointp{3,4};structPoint*ptrp;ptr-x10;//通过指针指向x并赋值ptr-y20;printf(x %d y %d\n,ptr-x,ptr-y);return0;}使用方式结构体指针-成员名综合举例#includestdio.h#includestring.hstructStu{charname[20];intage;};//通过点(.)打印内容voidprint_stu(structStus){printf(%s %d\n,s.name,s.age);}//通过-来访问结构体voidset_stu(structStu*ps){strcpy(ps-name,lisi);ps-age30;}intmain(){structStus{zhangsan,20};//为struct Stu进行初始化print_stu(s);set_stu(s);print_stu(s);return0;}2.结构体内存对齐接下来我们在讨论⼀个问题计算结构体的大小。这也是⼀个特别热门的考点结构体内存对⻬内存对齐的根本原因在于优化计算机硬件特别是CPU访问内存的效率并确保跨硬件平台的兼容性。2.1对齐规则⾸先得掌握结构体的对⻬规则结构体的第1个成员对⻬到和结构体变量起始位置偏移量为0的地址处。从第2个成员变量开始都要对⻬到某个对⻬数的整数倍的地址处。-对⻬数 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。- VS 中默认的值为 8- Linux中 gcc 没有默认对⻬数对⻬数就是成员⾃⾝的⼤⼩结构体总⼤⼩为最⼤对⻬数结构体中每个成员变量都有⼀个对⻬数所有对⻬数中最⼤的的整数倍。如果嵌套了结构体的情况嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处结构体的整体⼤⼩就是所有最⼤对⻬数含嵌套结构体中成员的对⻬数的整数倍。在Linux中gcc环境下1.成员按照自身的对齐值对齐如果不在对齐边界编译器会再加上填充字节2.结构体的总大小为结构体最宽基本类型成员大小的整数倍如有需要编译器会在最末一个成员之后加上填充字节trailing padding代码#includestdio.h//求结构体的对齐数intmain(){//练习1structS1{charc1;//1 8 1inti;//4 8 4charc2;//1 8 1};printf(%zu\n,sizeof(structS1));//12//练习2structS2{charc1;//1 8 1charc2;//1 8 1inti;//4 8 4};printf(%zu\n,sizeof(structS2));//8//练习3structS3{doubled;//8 8 8charc;//1 8 1inti;//4 8 4};printf(%zu\n,sizeof(structS3));//16//练习4-结构体嵌套问题structS4{charc1;//1 8 1structS3s3;//8 8 8doubled;//8 8 8};printf(%zu\n,sizeof(structS4));//32return0;}示例图如struct S12.2为什么存在内存对齐大部分参考资料解释如下1、平台原因 (移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常。2、性能原因数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对⻬的内存处理器需要作两次内存访问⽽对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数那么就可以⽤⼀个内存操作来读或者写值了。否则我们可能需要执行两次内存访问因为对象可能被分放在两个8字节内存块中。总体来说结构体的内存对齐是拿空间来换取时间的做法。那在设计结构体的时候我们既要满足齐又要节省空间如何做到?答让占用空间小的成员尽量集中在⼀起。例//例如structS1{charc1;inti;charc2;};structS2{charc1;charc2;inti;};};S1和S2类型的成员⼀模⼀样但是S1和 S2 所占空间的⼤⼩有了⼀些区别。2.3修改默认对齐数注在vs中可以修改在Linux中没有对齐数#includestdio.h#pragmapack(1)//设置默认对⻬数为1structS{charc1;inti;charc2;};#pragmapack()//取消设置的对⻬数还原为默认intmain(){//输出的结果是什么printf(%d\n,sizeof(structS));return0;}结构体在对齐方式不合适的时候我们可以自己更改默认对齐数。3.结构体传参我们首先来看一段代码#includestdio.hstructS{intdata[1000];intnum;};structSs{{1,2,3,4},1000};//结构体传参voidprint1(structSs){printf(%d\n,s.num);}//结构体地址传参voidprint2(structS*s){printf(%d\n,s-num);}intmain(){print1(s);//传结构体print2(s);//传地址return0;}上⾯的print1和print2函数哪个好些答案首选print2函数函数传参的时候参数是需要压栈会有时间和空间上的系统开销。如果传递⼀个结构体对象的时候结构体过大参数压栈的的系统开销比较大所以会导致性能的下降。结论结构体传参的时候要传结构体的地址。4.结构体实现位段4.1什么是位段位段的声明和结构是类似的有两个不同位段的成员必须是 int、unsigned int 或signed int 在C99中位段成员的类型也可以选择其他整型家族类型⽐如char。位段的成员名后边有⼀个冒号和⼀个数字。比如structA{int_a:2;int_b:5;int_c:10;int_d:30;};A就是一个位段类型。那位段A所占内存的大小是多少printf(%d\n,sizeof(structA));//输出84.2位段的内存分配①位段的成员可以是intunsignedintsigned int或者是char等类型②位段的空间上是按照需要以4个字节 int 或者1个字节 char 的⽅式来开辟的。③位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段。//⼀个例⼦structS{chara:3;charb:4;charc:5;chard:4;};structSs{0};s.a10;s.b12;s.c3;s.d4;//空间是如何开辟的//输出printf(%d\n,sizeof(structS));//3示例图4.3位段的跨平台问题①int 位段被当成有符号数还是无符号数是不确定的。②位段中最大位的数目不能确定。16位机器最⼤1632位机器最⼤32写成27在16位机器会出问题。③位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。④当⼀个结构包含两个位段第⼆个位段成员⽐较⼤⽆法容纳于第⼀个位段剩余的位时是舍弃剩余的位还是利⽤这是不确定的。总结跟结构相比位段可以达到同样的效果并且可以很好的节省空间但是有跨平台的问题存在。4.4位段的应用下图是网络协议中IP数据报的格式我们可以看到其中很多的属性只需要几个bit位就能描述这里使用位段能够实现想要的效果也节省了空间这样网络传输的数据报大小也会较小⼀些对网络的畅通是有帮助的。4.5位段使用的注意事项位段的几个成员共有同⼀个字节这样有些成员的起始位置并不是某个字节的起始位置那么这些位置处是没有地址的。内存中每个字节分配⼀个地址⼀个字节内部的bit位是没有地址的。所以不能对位段的成员使⽤操作符这样就不能使用scanf直接给位段的成员输⼊值只能是先输入放在⼀个变量中然后赋值给位段的成员。structA{int_a:2;int_b:5;int_c:10;int_d:30;};intmain(){structAsa{0};scanf(%d,sa._b);//这是错误的//正确的⽰范intb0;scanf(%d,b);sa._bb;return0;}总结位段就是让你按“位”来定义和使用内存的一种工具主要好处是省内存、方便位操作特别适合硬件寄存器、协议头解析和标志位打包。代价是牺牲了一些可移植性且不能取地址。在现代 C 编程中当需要精细控制某几个位时位段仍是一种有效的选择。最后本次结构体到这里就结束了有任何不懂的地方欢迎评论区一起讨论。路虽远行则将至。保持持续学习咱们下篇内容再会