ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

C语言梦开始的地方16:结构体

C语言梦开始的地方16:结构体 文章目录C语言梦开始的地方16结构体结构体类型的声明结构体变量创建和初始化结构体的特殊声明结构体的自引用结构体内存对齐对齐规则为什么存在内存对齐修改默认对齐数结构成员访问操作符结构体传参结构体实现位段什么是位段位段的内存分配位段的跨平台问题位段的应用位段使用的注意事项C语言梦开始的地方16结构体结构体类型的声明结构体是什么我们在了解C语言的数据类型章节时说到过C语言大致分为2种类型一种是内置类型即charint这种一种是自定义类型我们本章节的结构体就是C语言的自定义类型。那么结构体又是有什么组成的呢结构是一些值的集合这些值称为成员变量。结构的每个成员可以是不同类型的变量。结构体的声明structtag{member-list;}variable-list;比如我们现在要描述一个学生structStu{charname[20];// 名字intage;// 年龄charsex[2];// 性别charid[20];// 学号};结构体变量创建和初始化我们刚才已经创建了一个学生的结构体那么现在该如何对他进行初始化呢#includestdio.hstructStu{charname[20];// 名字intage;// 年龄charsex[2];// 性别charid[20];// 学号};intmain(){// 按结构体成员的顺序初始化structStus{张三,18,男,123456789};printf(name: %s\n,s.name);printf(age : %d\n,s.age);printf(sex : %s\n,s.sex);printf(id : %s\n,s.id);// 按照指定的顺序初始化structStus2{.age18,.name李四,.sex男,.id234567890};printf(name: %s\n,s2.name);printf(age : %d\n,s2.age);printf(sex : %s\n,s2.sex);printf(id : %s\n,s2.id);return0;}结构体的特殊声明在声明结构的时候可以不完全的声明。struct{inta;charb;floatc;}x;struct{inta;charb;floatc;}a[20],*p;这种声明常用于使用次数不多的场景比如只需要调用一次结构体的场景我们就可以使用上述声明这种声明在C语言中叫匿名结构体。声明的两个结构体参数都是一样的但是他们互不认识比如pb;这里就会导致程序编译报错。因为编译器在遇到第一个struct {...}时会在内部生成一个唯一的、隐式的类型名比如叫__Anonymous_Struct_1。当你再写第二个struct {...}时即使成员一模一样编译器会生成另一个完全不同的类型名__Anonymous_Struct_2。结构体的自引用那么在结构中包含一个类型为该结构本身的成员是否可以呢structNode{intdata;structNodenext;};上述代码正确吗如果正确那sizeof(struct Node) 是多少仔细分析其实是不行的因为一个结构体中再包含一个同类型的结构体变量这样结构体变量的大小就会无穷的大是不合理的。正确的自引用方式structNode{intdata;structNode*next;};为什么这样就可以呢因为指针长度是固定的平台是32位就是4字节64位就是8字节那么在结构体自引用使用的过程中夹杂了typedef 对匿名结构体类型重命名也容易引入问题看看下面的代码可行吗typedefstruct{intdata;Node*next;}Node;答案是不行的因为Node是对前面的匿名结构体类型的重命名产生的但是在匿名结构体内部提前使用Node类型来创建成员变量这是不行的。解决方法定义结构体不要使用匿名结构体了typedefstructNode{intdata;structNode*next;}Node;那么该代码和上述正常定义的结构体的区别在哪里难道就是多了个typedef吗区别在于后续编写代码时我们可以使用该结构体的定义的别名Node当我们在编写一个名字很长的结构体时很容易将名字打错我们就可以使用typedef来起别名。结构体内存对齐我们已经掌握了结构体的基本使用了。现在我们深入讨论一个问题计算结构体的大小。这也是一个特别热门的考点结构体内存对齐对齐规则首先我们要了解对齐规则结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处其他成员变量要对齐到某个数字对齐数的整数倍的地址处。对齐数 编译器默认的一个对齐数 与 该成员变量大小的较小值。VS 中默认的值为 8Linux中 gcc 没有默认对齐数对齐数就是成员自身的大小结构体总大小为最大对齐数结构体中每个成员变量都有一个对齐数所有对齐数中最大的的整数倍。每个成员的对齐数只取‘自己大小’和‘默认数’的较小值整个结构体的大小最后再按所有成员中‘最大的那个对齐数’来补齐。如果嵌套了结构体的情况嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体中成员的对齐数的整数倍。注意当结构体成员变量大小超过了默认对齐数时按默认对齐数,练习1structS1{charc1;inti;charc2;};c1 占第0字节。i 大小为4对齐数为4必须从偏移量为4的整数倍即4开始所以偏移量1~3被浪费填充。c2 大小为1对齐数为1紧跟 i 后面偏移量为8。总大小为最大对齐数4的整数倍当前为9向上取整到最接近的4的倍数 → 12字节。练习2structS2{charc1;charc2;inti;};c1 在0c2 大小为1对齐数为1紧跟在偏移量1。i 对齐数为4必须从偏移量4开始偏移量2~3被浪费。总大小为4的整数倍偏移量448 → 8字节。对比发现S1和S2成员一样但顺序不同大小差了4字节证明“让占用小的成员集中”能省空间。所以在定义结构体时同类型应该要集中在一起为什么存在内存对齐硬件原因某些CPU如ARM或老式x86在读取未对齐的 int 或 double 时会触发总线错误或进行多次读取拼接。性能原因现代CPU访问内存时通常以“字长”如8字节为单位读取。对齐的数据可以一次性载入寄存器未对齐的数据可能跨越两个内存块需要两次读取再拼接效率大打折扣。结构体对齐本质是用空间换取时间。修改默认对齐数当需要节省空间如网络通信数据包、文件存储结构时可以使用#pragma来设置默认对齐。此时结构体大小就是所有成员大小之和无填充。但代价是CPU读取效率会下降。#includestdio.h#pragmapack(1)// 设置默认对齐数为1structS{charc1;inti;charc2;};#pragmapack()//取消设置的对齐数还原为默认intmain(){printf(%d\n,sizeof(structS));return0;}当结构体在对齐方式不合适的时候我们可以自己更改默认对齐数。结构成员访问操作符结构成员访问操作符.操作符左操作数是结构体变量或结构体数组的元素右操作数是成员名。编译器会根据成员类型计算相对于结构体起始地址的偏移量然后进行寻址。-操作符左操作数必须是结构体指针。它等价于 (*ps).num但书写更简洁。在操作系统中频繁传递大结构体的指针时- 的使用频率极高。那么什么样的场景使用它们俩呢左边是 struct 变量就用.左边是 struct* 指针就用-#includestdio.hstructStu{charname[20];// 名字intage;// 年龄charsex[2];// 性别charid[20];// 学号};intmain(){structStus{张三,18,男,123456789};printf(name: %s\n,s.name);printf(age : %d\n,s.age);printf(sex : %s\n,s.sex);printf(id : %s\n,s.id);structStu*s2s;printf(name: %s\n,s2-name);printf(age : %d\n,s2-age);printf(sex : %s\n,s2-sex);printf(id : %s\n,s2-id);return0;}结构体传参同样结构体也可以当作函数的参数使用那么现在有个问题就是结构体这个自定义类型是传值好还是传址好呢我们看下面这个代码#includestdio.hstructS{intdata[1000];intnum;};structSs{{1,2,3,4},1000};// 结构体传参voidprint1(structSs){printf(%d\n,s.num);}// 结构体地址传参voidprint2(structS*ps){printf(%d\n,ps-num);}intmain(){print1(s);// 传结构体print2(s);// 传地址return0;}运行程序的时候感觉没事区别结果都是瞬间出来。这是因为现代CPU处理数据都非常快快到你点击运行程序时程序就已经跑完了。那么我们从底层来看到底谁更好传值调用print1实参 s 的内容包含一个1000个int的数组会被完整复制到栈帧函数调用栈中。这涉及 1000 * 4 4000 字节的内存拷贝极其耗费CPU时间和栈空间容易导致栈溢出。传址调用print2只传递一个指针4或8字节压栈开销极小。函数内部通过指针间接访问数据效率极高。总结当结构体成员很少时比如只有一个char类型这种情况以外永远优先传递结构体指针。如果你不想让函数修改原结构体内容可以在形参前加const修饰如void print2(const struct S* ps)。结构体实现位段结构体了解完就得了解了解结构体实现位段的能力。什么是位段位段的声明和结构是类似的有两个不同位段的成员必须是int、unsigned int 或signed int 在C99中位段成员的类型也可以选择其他类型。冒号后的数字表示该成员占用的比特位数不能超过该类型的总位数。比如structA{int_a:2;int_b:5;int_c:10;int_d:30;};这里A就是位段类型。那么位段A所占内存的大小是多少printf(%d\n,sizeof(structA));答案是8为什么呢成员类型是 int编译器按 4 字节32 bit开辟空间。_a(2) _b(5) _c(10) 17 bit小于 32都放在第一个“字”里。_d 需要 30 bit但第一个字只剩 32 - 17 15 bit放不下 30 bit所以舍弃剩余 15 bit重新开辟第二个 32 bit 给 _d。总大小 8 字节两个 int位段的内存分配为什么上述代码得出的是8字节如果这里给任意的整型会发生什么呢要了解这个问题我们就需要先了解位段的内存是如何分配的。位段的成员可以是int unsigned int signed int 或者是char 等类型位段的空间上是按照需要以4个字节 int 或者1个字节 char 的方式来开辟的。位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段。2的意思是当前变量是什么类型就按当前这个类型来开辟空间所以创建位段时成员变量最好都是同类型的。那我们了解了位段的内存分配那么空间是如何开辟的呢请看下面示例structS{chara:3;charb:4;charc:5;chard:4;};intmain(){structSs{0};s.a10;s.b12;s.c3;s.d4;return0;}上述环境为visual studio2022 平台上位段的跨平台问题int 位段被当成有符号数还是无符号数是不确定的。位段中最大位的数目不能确定。16位机器最大1632位机器最大32写成27在16位机器会出问题。位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。总结跟结构相比位段可以达到同样的效果并且可以很好的节省空间但是有跨平台的问题存在。位段的应用下图是网络协议中IP数据报的格式我们可以看到其中很多的属性只需要几个bit位就能描述这里使用位段能够实现想要的效果也节省了空间这样网络传输的数据报大小也会较小一些对网络的畅通是有帮助的。除了上述用途以外位段的应用还有单片机文件格式等场景。位段是“距离硬件最近的C语言特性”除非你在写底层驱动、芯片寄存器映射或文件系统否则在普通工程里请用位运算、|、来代替位段保证代码绝对可移植。位段使用的注意事项位段的几个成员共有同一个字节这样有些成员的起始位置并不是某个字节的起始位置那么这些位置处是没有地址的。内存中每个字节分配一个地址一个字节内部的bit位是没有地址的。所以不能对位段的成员使用操作符这样就不能使用scanf直接给位段的成员输入值只能是先输入放在一个变量中然后赋值给位段的成员。#includestdio.hstructA{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;}为什么不能直接sa.b呢因为C语言中是按字节编号的。位段的成员可能只占据一个字节中间的某几个bit比如从第3bit到第5bit这个位置没有对应的字节地址。因此sa._b是非法的。正确的输入方法定义一个普通整型变量int b;用scanf(%d, b);接收用户输入然后通过sa._b b;将值赋给位段成员赋值时会自动截断至指定的bit位。完
返回列表