核心内容摘要
黑土吃“钢筋”惊呆众人
结构体的介绍C语⾔已经提供了内置类型如char、short、int、long、float、double等但是只有这些内置类 型还是不够的假设我想描述学⽣描述⼀本书这时单⼀的内置类型是不⾏的。
描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等 描述⼀本书需要书名、作者、出版社、定价等。
C语⾔为了解决这个问题增加了结构体这种⾃定义的数据类型让程序员可以⾃⼰创造适合的类型。
结构是⼀些值的集合这些值称为成员变量。
结构的每个成员可以是不同类型的变量如 标量、数组、指针甚⾄是其他结构体。
1 结构的声明struct tag { member-list; //成员列表 }variable-list;//变量列表、描述一个学生我们可以这样声明struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 };//注意分号不能丢弃
11 结构体的特殊声明在声明结构的时候可以不完全的声明。
即省略了结构体标签tag比如//匿名结构体类型 struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20], *p;这两个结构在声明时都省略了结构体标签那么问题来了//在上⾯代码的基础上下⾯的代码合法吗 p x;我们生成解决方案发现表明虽然它们的成员列表相同但是编译器认为这是两种不同的类型。
所以这段代码是不合法的。
匿名的结构体类型如果没有对结构体类型重命名的话基本上只能使⽤⼀次。
//进行重命名 typedef struct { char a; int b; float c; }s;//将这种结构体类型重命名为s int main() { //将匿名的结构体重命名后 //得到的s就是一种类型 s s1; s s2;//既然s是一种结构体类型而不是匿名的结构体类型那我们就可以多次使用了 return 0; }所以建议定义结构体不要使用匿名结构体了
2 结构体变量的定义与初始化//代码1变量的定义 struct Point { int x; int y; }p1; //声明类型的同时定义变量p1 struct Point p2; //定义结构体变量p2 //代码2:初始化。
struct Point p3 {10, 20}; struct Stu //类型声明 { char name[15];//名字 int age; //年龄 }; struct Stu s1 {zhangsan, 20};//初始化 -- 按照成员列表的顺序进行初始化 struct Stu s2 {.age20, .namelisi};//指定顺序初始化 (用点操作符进行初始化 //代码3 struct Node { int data; struct Point p; struct Node* next; }n1 {10, {4,5}, NULL}; //结构体嵌套初始化 struct Node n2 {20, {5, 6}, NULL};//结构体嵌套初始化
3 结构的自引用在结构中包含⼀个类型为该结构本⾝的成员是否可以呢⽐如定义⼀个链表的节点struct Node { int data; struct Node next; };这段代码正确吗如果正确那么sizeof(struct Node)的值又是多少呢仔细分析其实是不⾏的因为⼀个结构体中再包含⼀个同类型的结构体变量这样结构体变量的大小就会⽆穷的⼤是不合理的。
正确的自引用方式struct Node { int data; struct Node* next;//通过记录下一个节点的地址就可以找到下一个节点的结构体 };
结构成员访问操作符
1 结构体成员的直接访问结构体成员的直接访问是通过点操符.访问的。
点操作符接受两个操作数。
如下所示#include stdio.h struct Point { int x; int y; }p {1,2}; int main() { printf(x: %d y: %d\n, p.x, p.y); return 0; }使⽤⽅式结构体变量.成员名 中间是点操作符
2 结构体成员的间接访问通过指针有时候我们得到的不是⼀个结构体变量⽽是得到了⼀个指向结构体的指针。
如下所⽰#include stdio.h struct Point { int x; int y; }; int main() { struct Point p {3, 4}; struct Point *ptr p; ptr-x 10;//通过指针找到 x 的空间再进行赋值操作 ptr-y 20;//通过指针找到 y 的空间再进行赋值操作 printf(x %d y %d\n, ptr-x, ptr-y); return 0; }使⽤⽅式结构体指针 - 成员名
结构体内存对齐重点介绍通过上面的学习我们已经掌握结构体的基本使用了我们现在深入讨论一个问题既然结构体是一种自定义类型那么类型是要有大小的那我们如何计算结构体的大小这也是⼀个特别热⻔的考点结构体内存对齐
1 对齐规则首先得掌握结构体的对⻬规则
结构体的第1个成员对齐到和结构体变量起始位置偏移量为0的地址处。
从第2个成员变量开始都要对⻬到某个对齐数的整数倍的地址处。
对⻬数 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较小值。
VS 中默认的值为
Linux中 gcc 没有默认对⻬数对⻬数就是成员⾃⾝的⼤⼩
结构体总⼤⼩为最⼤对⻬数结构体中每个成员变量都有⼀个对⻬数所有对⻬数中最⼤的的整数倍。
如果嵌套了结构体的情况嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处结构 体的整体⼤⼩就是所有最⼤对⻬数含嵌套结构体中成员的对⻬数的整数倍。
11 练习巩固接下来通过讲解练习来帮助我们巩固对齐规则在vs2022的环境下默认对齐数为8struct S2 { char c1; char c2; int i; }; //该结构体的大小是多少个字节呢题解假设起始位置后
第一个成员是char c1 ,按照第一条规则第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
又因为char类型大小为一个字节故如图所示C1在内存中的位置如上
从第二个成员变量开始都要对⻬到某个对齐数的整数倍的地址处。
由于vs默认对齐数为8而char类型的对齐数类型大小为1取其中较小值即为1又因为此时偏移量为1为1的整数倍故C2直接位于较起始位置偏移量为2的位置如图
对于int i 由于默认对齐数为8而int类型的对齐数为4取其中较小值则为4故 i 的存储要从偏移量为4的整数倍开始存储又上述可知存完前两个变量后偏移量为2故我们要浪费2个字节的空间此时偏移量为4刚好为4的整数倍然后我们在往后取4个字节的空间来存放 i 综上总的偏移量为 1 1 2浪费的空间 4 8 个字节又因为此时的总偏移量 8 刚好为最大对齐数 4 的整数倍故该结构体的大小为8个字节。
有了上述的讲解后我们就可以对对齐规则有更加深入的了解了接下来进行做练习巩固只提供图解不提供详细文字讲解自己思考//例如 struct S1 { char c1; int i; char c2; }; //该结构体的大小为多少呢图解不能忘记总偏移量要为最大对齐数的整数倍这里在c2存储好后总偏移量为9 并不是 最大对齐数 4 的整数倍故我们为此又多浪费了3 个字节 保证偏移量为 12 是 4的 整数倍。
思考以上两个练习的结构体的成员列表相同但是为什么结构体的大小不同呢答案见 为什么存在内存对齐练习 3//练习3 struct S3 { double d; char c; int i; }; printf(%zu\n, sizeof(struct S
);//大小16个字节有了上述的思考那么这个问题应该不难了。
练习4 -- 结构体的嵌套问题struct S4 { char c1; struct S3 s3; double d; }; printf(%zu\n, sizeof(struct S
);//大小为32个字节 // //其实做法和前面的练习是一样的 //但是要注意 //第二个成员变量即结构体类型的变量它的对齐数为它成员列表里的最大对齐数 //由练习三可知 最大对齐数为 8 即double类型的大小解题过程和前几个练习相同自行思考
12 offsetof (宏可以用来计算偏移量offsetof ----宏可以计算出结构体成员与起始位置的偏移量offsetof(type,member),使用时需要包含头文件stddef.h其中type为结构体类型member为成员变量名我们在写题过程中可以利用offsetof来计算每个成员的偏移量来验证我们的猜想下面拿练习4来举例#includestdio.h #includestddef.h struct S3 { double d; char c; int i; }; struct S4 { char ci;//对齐数为1 struct S3 s3;//对齐数为8大小为16个字节 double d;//对齐数为8 }; int main() { printf(%zu\n, sizeof(struct S
);//32 //offsetof ---- 宏可以计算出结构体成员与起始位置的偏移量 //offsetof(type,member),使用时需要包含头文件stddef.h printf(%zu\n, offsetof(struct S4, ci));//0 printf(%zu\n, offsetof(struct S4, s
);//8 printf(%zu\n, offsetof(struct S4, d));//24 return 0; }
2 为什么存在内存对齐⼤部分的参考资料都是这样说的
平台原因 (移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定 类型的数据否则抛出硬件异常。
性能原因数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。
原因在于为了访问未对⻬的内存处理器需要 作两次内存访问⽽对⻬的内存访问仅需要⼀次访问。
假设⼀个处理器总是从内存中取8个字节则地 址必须是8的倍数。
如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数那么就可以 ⽤⼀个内存操作来读或者写值了。
否则我们可能需要执⾏两次内存访问因为对象可能被分放在两 个8字节内存块中。
总体来说结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候我们既要满⾜对⻬⼜要节省空间如何做到 让占⽤空间⼩的成员尽量集中在⼀起//例如 struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };S1 和 S2 类型的成员⼀模⼀样但是 S1 和 S2 所占空间的⼤⼩有了⼀些区别。
这也就回答了我们上述的思考。
3 修改默认对⻬数#pragma 这个预处理指令可以改变编译器的默认对⻬数。
注意设置默认对齐数后还需要取消设置的对齐数还原为默认#include stdio.h #pragma pack(
//设置默认对⻬数为1 struct S { char c1; int i; char c2; }; #pragma pack()//取消设置的对⻬数还原为默认 int main() { //输出的结果是什么 printf(%d\n, sizeof(struct S)); return 0; }通过改变默认的对齐数那么对齐数可能就会发生改变。
结构体在对⻬⽅式不合适的时候我们可以⾃⼰更改默认对⻬数。