第20讲:自定义类型:结构体

发布时间:2026/7/4 20:05:27
第20讲:自定义类型:结构体 1.结构体类型的声明1.1 结构体结构是一些值的集合这些值称为成员变量。结构的每个成员可以是不同类型的变量。结构的声明struct tag{member-list;}variable-list;// 结构体类型的声明 -- 假设描述一个学生structStu{charname[20];// 姓名intage;// 年龄charsex[5];// 性别charid[20];// 学号};// 分号不能丢结构体变量的创建和初始化注意结构体的声明之后使用前必须手动赋值否则是随机未知的值。// 结构体变量的创建structStu{charname[20];intage;charsex[5];charid[20];}s3,s4;// 方式三// 方式二全局变量structStus2;// 结构体变量的初始化intmain(){// 方式一局部变量// 初始化方式一按照结构体成员的顺序初始化structStus1{张三,20,男,20230818001};printf(name: %s\n,s1.name);// 张三printf(age: %d\n,s1.age);// 20printf(sex: %s\n,s1.age);// 男printf(id: %s\n,s1.id);// 20230818001// 初始化方式二按照指定的顺序初始化structStus5{.age18,.namelisi,.id20230818002,.sex女};printf(name: %s\n,s1.name);// lisiprintf(age: %d\n,s1.age);// 18printf(sex: %s\n,s1.age);// 女printf(id: %s\n,s1.id);// 20230818002return0;}2.结构的特殊声明在声明结构的时候可以不完全的声明。匿名结构体类型 - 只能创建一次结构体后续不能再创建结构体struct{inta;charb;floatc;}x;struct{inta;charb;floatc;}*px;警告编译器会把上面的两个声明当成完全不同的两个类型所以是非法的。匿名的结构体类型如果没有对结构体重命名的话基本上只能使用一次。可以使用typedef进行类型重定义操作typedefstruct{inta;charb;floatc;}S;intmain(){S s1{4,a,4.0};return0;}3.结构的自引用在结构体中包含一个类型为该结构体本身的成员是否可以比如定义一个链表的节点代码块 struct Node { int data; struct Node next; // 访问下一个节点 };其实这是不行的因为一个结构体中再包含一个同类型的结构体变量这样结构体变量的大小就会无穷的大是不合理的。正确的自引用示范代码块 struct Node { int data; struct Node* next; // 访问下一个节点指针 };结构体自引用使用过程中夹杂了typedef对匿名结构体类型重命名也容易引入问题比如下面代码。typedef struct { int data; Node* next; }Node;因为Node是对前面匿名结构体的重命名产生的但是在匿名结构体内部提前使用Node类型来创建成员变量这是不行的。解决办法定义结构体不要使用匿名结构体了。typedef struct Node { int data; struct Node* next; }Node; int main() { Node n1; printf(%zu\n, sizeof(struct Node)); // 16 return 0; }4.结构体内存对齐Structure Memory Alignment本节深入讨论一个问题计算结构体的大小。这是特别热门的考点结构体内存对齐。补充知识点offset()C语言标准宏 – offset of member 成员偏移量函数原型#define offsetof(type, member)((size_t)((type*)0)-member)参数type结构体类型member结构体里的成员返回值size_t类型该成员相对于结构体首地址的字节偏移量。本质是宏不是函数。包含头文件stddef.h宏和函数的核心区别本质区别宏Macro是预处理阶段的文本替身没有类型检查直接简单替换字符串。函数Function是编译阶段编译指令有函数调用栈、参数压栈、返回值有类型检查。详细对比2.1 执行阶段不同宏预处理阶段编译前直接文本替换函数编译运行阶段运行时调用2.2 参数处理不同宏无类型检查原样替换容易出现运算优先级Bug。如#define ADD(a, b) abADD(2, 3)*4 — 23*4 14函数有类型检查参数先计算值在传入2.3 开销不同宏无调用开销直接展开代码速度快、但代码冗余多处使用会复制多份函数有调用开销压栈、跳转、返回代码复用体积更小。2.4 作用域与安全宏无作用域全局生效不做参数计算会重复执行。函数参数值计算一次安全有作用域。2.5 调试宏不能断点调试展开后才是调试函数可正常断点调试4.1 对齐规则首先得掌握结构体的对齐规则结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。从第2个成员变量开始都要对齐到某个对齐数的整数倍的地址处。对齐数 编译器默认的一个对齐数 与 该成员变量大小 的较小值VS中默认的值为8Linux中gcc没有默认对齐数对齐数就是成员自身的大小结构体总大小为最大对齐数结构体中每个成员变量都有一个对齐数所有对齐数中最大的的整数倍。如果嵌套了结构体的情况嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体中成员的对齐数的整数倍。#includestddef.h#includestdio.hstructS1{// 成员变量大小 VS默认对齐数 对齐数charc1;// 1 8 1inti;// 4 8 4charc2;// 1 8 1}intmain(){// 查看结构体成员变量对齐的起始偏移量printf(%zu\n,offset(structS1,c1))// 0printf(%zu\n,offset(structS1,i))// 4printf(%zu\n,offset(structS1,c2))// 8printf(%zu\n,sizeof(structS1));// 12}分析该结构体变量的对齐数为4所以第二个变量在偏移量为4的地址处所以该变量的大小为 13(浪费)419 - 最大对齐数为4结构体总大小为4*312#includestddef.h#includestdio.hstructS2{// 成员变量大小 VS默认对齐数 对齐数charc1;// 1 8 1charc2;// 1 8 1inti;// 4 8 4};intmain(){printf(%zu\n,offsetof(structS2,c1));// 0printf(%zu\n,offsetof(structS2,c2));// 1printf(%zu\n,offsetof(structS2,i));// 4printf(%zu\n,sizeof(structS2));// 8return0;}分析c1的偏移量是0c2的偏移量是1i的对齐数是4所以偏移量是4结构体变量大小是112(浪费)48 - 最大对齐数是4结构体总大小为4*28#includestddef.h#includestdio.hstructS3{// 成员变量大小 VS默认对齐数 对齐数doubled;// 8 8 8charc;// 1 8 1inti;// 4 8 4};intmain(){printf(%zu\n,offsetof(structS3,d));// 0printf(%zu\n,offsetof(structS3,c));// 8printf(%zu\n,offsetof(structS3,i));// 12printf(%zu\n,sizeof(structS3));// 16return0;}分析第一个成员变量d的偏移量是0c的偏移量是8i的偏移量是12结构体变量大小是817(浪费)412最大对齐数是8所以结构体变量总大小是8*216#includestddef.h#includestdio.hstructS3{// 成员变量大小 VS默认对齐数 对齐数doubled;// 8 8 8charc;// 1 8 1inti;// 4 8 4};structS4{// 成员变量大小 VS默认对齐数 对齐数charc1;// 1 8 1structS3s3;// 16 8 8doubled;// 4 8 4};intmain(){printf(%zu\n,sizeof(structS3,c1));// 0printf(%zu\n,sizeof(structS3,s3));// 8printf(%zu\n,sizeof(structS3,d));// 24printf(%zu\n,sizeof(structS3));// 32return0;}分析d的偏移量是0c的偏移量是17i的偏移量是816243*8结构体变量大小是17(浪费)16832最大对齐数是8结构体总大小是324.2 为什么存在内存对齐大部分参考资料都是这样说的平台原因移植原因不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则会抛出硬件异常。性能原因数据结构尤其是栈应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存处理器需要作两次内存访问而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节则地址必须是8的整数倍。如果能保证将所有的double类型的数据的地址都对齐成8的整数倍那么就可以用一个内存操作来读或者写值了。否则可能需要执行两次内存访问。因为对象可能被分放在两个8字节内存块中。总体来说结构体的内存对齐是拿空间来换取时间的做法。在设计结构体的时候既要满足对齐又要节省空间如何做到让占用空间小的成员尽量集中在一起。(成员顺序可以调整的情况)4.3 修改默认对齐数#pragma 这个预处理指令可以改变编译器的默认对齐数。一般修改的对齐数都是2的次方数1、2、4、8、。。。// 设置默认对齐数为1#pragmapack(1)structS{charc1;// 1 1 1inti;// 4 1 1charc2;// 1 1 1};// 取消设置的对齐数还原为默认。#pragmapack()intmain(){printf(%zu\n,sizeof(structS));// 6return0;}5.结构体传参函数传参的时候参数是需要压栈会有时间和空间上的系统开销。如果传递一个结构体对象的时候结构体过大参数压栈的系统开销比较大所以会导致性能的下降。结论结构体传参的时候要传结构体的地址。推荐print2()structS{intdata[1000];intnum;};// 值传递 - 不推荐还要创建一次变量可能结构体过大voidprint1(structSt){for(inti0;i5;i){printf(%d ,t.data[i]);}printf(\n);printf(%d\n,t.num);}// 址传递 - 推荐不会额外开辟空间voidprint2(conststructS*ps){for(inti0;i5;i){printf(%d ,ps-data[i]);}printf(\n);printf(%d\n,ps-num);}intmain(){structSs{{1,2,3,4,5},100};print1(s);print2(s);return0;}6.结构体实现位段6.1 什么是位段位段的声明和结构体是类似的有两个不同位段的成员必须是int、unsigned int或signed int在C99中位段成员的类型也可以选择其他整形家族类型比如char。位段的成员名后边有一个冒号和数字。语法strcut 位段名 { 类型 成员名 : 占用位数; // 核心冒号数字指定位数 };注意占用位数不能超过类型本身的位数如int最多32位structB{int_a;// 32bitint_b;// 32bitint_c;// 32bitint_d;// 32bit};structA{int_a:2;// 1字节int_b:5;// 1字节int_c:10;// 2字节int_d:30;// 4字节};// 25103047bitintmain(){printf(%zu\n,sizeof(structB));// 16printf(%zu\n,sizeof(structA));// 8return0;}6.2 位段的内存分配位段的成员可以是int、unsigned int、signed int或者char等所有整型类型。位段的空间上是按照需要以4字节int或者1个字节char的方式来开辟。位段涉及很多不稳定因素位段是不跨平台的注意可移植的程序应该避免使用位段。vs环境下每个字节分配的内存空间是从右向左使用的低位向高位剩余的空间不够下一个成员使用的时候直接浪费。6.3 位段的跨平台问题int位段被当成有符号数还是无符号数是不确定的。位段中最大的数目不能确定早期16位机器int最大16,32位机器最大32写成27在16为机器会出问题。位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。总结跟结构相比位段可以达到同样的效果并且可以很好的节省空间。但是有跨平台的问题存在。structS{chara:3;charb:4;charc:5;chard:4;};/* 解析内存中每个字节中假设从右向左使用 b a c d 0 1100 010 |000 0 0011 |0000 0100 内存中是 6 2 0 3 0 4 小端存储为62 03 04占用3个字节 */intmain(){structSs{0};s.a10;// 0000 1010s.b12;// 0000 1100s.c3;// 0000 0011s.d4;// 0000 0100return0;}6.4 位段的应用网络协议IP数据报的格式很多的属性只需要几个bit位就能描述这里使用位段能够实现想要的效果也节省了空间这样网络传输的数据报大小也会较小一些对网络的畅通是有帮助的。6.5 位段使用的注意事项位段的几个成员共用同一个字节这样有些成员的起始位置并不是某个字节的起始位置那么这些位置处是没有地址的。内存中每个字节分配一个地址一个字节内部的bit位是没有地址的。所以不能对位段的成员使用操作符这样就不能scanf直接给位段的成员输入值只能是先输入放在一个变量中然后赋值给位段的成员。structC{int_a:2;int_b:5;int_c:10;int_d:30;};intmain(){structCsa{0};//scanf(%d, sa._a); // err// 正确的示范intb0;scanf(%d,b);sa._bb;return0;}7.练习练习1变种水仙花数变种水仙花数 - Lily Number把任意的数字从中间拆分成两个数字比如1461可以拆分成1和461,14和61,146和1),如果所有拆分后的乘积之和等于自身则是一个Lily Number。例如655 6 * 55 65 * 51461 1461 1461 146*1求出5位数中的所有 Lily Number。输入描述无输出描述一行5位数中的所有 Lily Number每两个数之间间隔一个空格。#includemath.h#includestdio.hintmain(){// 1. 遍历五位数的整数for(inti10000;i99999;i){intret0;// 保存加起来的和intexp1;// 10的指数inta0// 拆分五位数的前半部分intb0;// 拆分五位数的后半部分while(exp5){ai/pow(10,exp);bi%pow(10.exp);reta*b;exp;}// 判断是否是变种水仙花数将其打印出来if(reti){printf(%d ,i);}}return0;}// 方式二intmain(){for(inti10000;i99999;i){intsum0;for(intj10;j10000;j*10){sumsum(i/j)*(i%j);}if(sumi){printf(%d ,i);}}}2.练习2序列中去除指定数字描述有一个整数序列可能有重复的整数现删除指定的某一个整数输出删除指定数字之后的序列序列中未被删除数字的前后位置没有发生改变。若序列中有多个指定的数需要一起删除。数据范围序列长度和序列中的值都满足1≤n≤50输入描述第一行输入一个整数(0≤N≤50)。第二行输入N个整数输入用空格分隔的N个整数。第三行输入想要进行删除的一个整数。输出描述输出为一行删除指定数字之后的序列。intmain(){// 1.输入整数intN;scanf(%d,N);// 2.输入N个整数intarr[N];// 可变数组VS不支持可以改成int arr[50] { 0 };for(inti0;iN;i){scanf(%d,arr[i]);}// 3.输入需要删除的数字intdel;scanf(%d,del);intj0;for(inti0;iN;i){if(arr[i]!del){arr[j]arr[i];j;}}return0;}intmain(){// 1.输入整数intN;scanf(%d,N);// 2.输入N个整数intarr[N];// 可变数组VS不支持可以改成int arr[50] { 0 };for(inti0;iN;i){scanf(%d,arr[i]);}// 3.输入需要删除的数字intdel;scanf(%d,del);for(inti0;iN;i){if(arr[i]del){// 将后面的序列全部向前移一位for(intki;kN-1;k){arr[k]arr[k1];}N--;i--;}}return0;}莫因表格遮前路代码长明赴山海