C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)一:https://developer.aliyun.com/article/1530419
结构体内存对齐
深入讨论一个问题:计算结构体的大小
struct S1 { char c; //1字节 int i; //4字节 char c2;//1字节 }; int main() { struct S1 s1 = { 0 }; printf("%d\n", sizeof(s1)); return 0; }
结果显示为12,这涉及到了结构体内存对齐的知识点。我们先了解结构体内存对齐的规则
对齐规则
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。要更好地理解这个对齐规则,我们要先简单了解一些概念。
偏移量
对齐数
对齐数= 编译器默认的一个对齐数与该成员大小的 较小值。
VS中默认的对齐数的值为8,;而Linux没有默认对齐数的概念。
这个时候我们拿起刚刚那道题,用对齐规则来解决。
练习
struct S2 { char c; int i; double d; }; struct S3 { double d; char c; int i; }; int main() { struct S2 s2 = {0}; struct S3 s3 = {0}; }
试着算算s2和s3的大小,答案在后文公布。
嵌套结构体大小计算
struct S4 { char c1; struct S3 s3; double d; }; int main() { struct S4 s4 = { 0 }; printf("s4 = %d\n", sizeof(s4)); return 0; }
练习答案
嵌套结构体计算大小:
存在内存对齐的原因
1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次访问;而对齐的内存访问仅需要一次访问。
对“访问未对齐的内存,处理器需要作两次访问;而对齐的内存访问仅需要一次访问。”做解释:
总的来说
结构体的内存对齐是拿空间来换取时间的做法。
结构体的设计
在设计结构体的时候,我们既要满足对齐,又要节省空间。
即,让占用空间小的成员尽量集中在一起
例如:
struct S1 { char c1; int n; char c2; }s1; struct S2 { char c1; char c2; int n; }s2; int main() { printf("s1 = %u\n", sizeof(s1)); printf("s2 = %u\n", sizeof(s2)); return 0; }
修改默认对齐数
使用#pragma这个预处理指令,可以改变默认对齐数。
具体为:
#include <stdio.h> #pragma pack(2) //修改默认对齐数为2 struct S1 { char c1; int n; char c2; }s1; #pragma pack() //取消设置的默认对齐数,还原为8 struct S2 { char c1; int n; char c2; }s2; int main() { printf("s1 = %u\n", sizeof(s1));//默认对齐数为2时的大小 printf("s1 = %u\n", sizeof(s1));//默认对齐数为8时的大小 return 0; }
变量 |
占用大小 |
默认对齐数 |
对齐数 |
c1 |
1 |
8 |
1 |
n |
4 |
8 |
4 |
c2 |
1 |
8 |
1 |
变量 |
占用大小 |
默认对齐数 |
对齐数 |
c1 |
1 |
2 |
1 |
n |
4 |
2 |
2 |
c2 |
1 |
2 |
1 |
结论
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。
结构体传参
试着对比下面两种传参方式
#include <stdio.h> struct S { int data[1000]; int num; }; struct S s = { {1,2,3,4},1000 }; //使用结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //使用结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print1(s);//传结构体 print2(&s);//传地址 return 0; }
我们应该首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
而且,直接传结构体会有一些限制。例如,并不能改变结构体内部成员的值,而传地址既节省系统开销,提高性能,又不会有太多限制。
#include <stdio.h> struct S { int data[1000]; int num; }; struct S s1 = { {1,2,3,4},1000 }; struct S s2 = { {1,2,3,4},1000 }; //结构体传参 修改成员变量的值 void change1(struct S s1) { s1.num = 2000; } //结构体地址传参 修改成员变量的值 void change2(struct S* ps) { ps->num = 2000; } int main() { change1(s1); change2(&s2); printf("s1.num = %d\n", s1.num); printf("s2.num = %d\n", s2.num); return 0; }
而当你使用结构体地址传参且不需要改变其成员变量的值时,可以加上const修饰。