结构体内存对齐
我们来思考一个问题:如何计算结构体的大小?
struct S1 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct S1)); //大小为多少呢? return 0; }
输出:12
为什么呢?
别急我们先来了解一下结构体的对齐规则:
- 第一个成员在与结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
(VS中默认的值为8)
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们来分析一下:
struct S1 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct S1)); return 0; }
我们可以做出下图:
从偏移量为0的地址处开始存放数据,第一个是 char 型,默认对齐数是8,1与8相比1小,对齐到1的整数倍0处,然后是 int 型,4与8比较4小,于是对齐到4的倍数处,即偏移量为4处(1、2、3处内存均被浪费了),然后是 cahr 型,对齐到偏移量为8处,存放完数据后发现总共占用了9个字节的空间,9不是4的倍数,于是扩大到12,即偏移量为11处(9、10、11处内存被浪费了)。
再来一道例题:
struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S2)); return 0; }
输出:8
原理和上题一样,多思考思考。
再来下题:
struct S3 { double d; char c; int i; }; int main() { printf("%d\n", sizeof(struct S3)); return 0; }
输出:16
注意:double 类型占用8字节的空间。
来个结构体嵌套问题:
struct S3 { double d; char c; int i; }; struct S4 { char c1; struct S3 s3; double d; }; int main() { printf("%d\n", sizeof(struct S4)); return 0; }
输出:32
画一画图就能解出了,解题关键在内存对齐规则4处,认真阅读,这里就不再分析了。
我们来思考个问题:为什么存在内存对齐?
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(比如:int型数据读取只能在偏移量为4的倍数处读取)
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
第二点什么意思呢?
我们来举个例子就知道了:
struct S { char c; int i; }; struct S s;
我们针对两种不同对齐方式做出下图:
以32位为例,一次读取4个字节
不对齐:读取数据 c 的时候,一次读取4个字节,很明显 c 被读取出来了,如果读取数据 i 呢?
首先从左边开始读取4个字节数据,但是我们发现一次读取似乎不能读完 i 的内容,需要再读一次,如图:
对齐:对齐情况如图:
读取数据 c 时,读取一次即可,读取数据 i 时,读取一次即可。
这样大大减少了时间,但是也浪费了空间。
由此:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
例如:
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
S2占用空间更小。
修改默认对齐数
我们先来了解下 #pragma 这个预处理指令,这个指令可以改变我们的默认对齐数。
例如:
#include <stdio.h> #pragma pack(8) //设置默认对齐数为8 struct S1 { char c1; int i; char c2; }s1; #pragma pack() //取消设置的默认对齐数,还原为默认
s1占用内存大小为12字节
#pragma pack(1)//设置默认对齐数为1 struct S2 { char c1; int i; char c2; }s2; #pragma pack()//取消设置的默认对齐数,还原为默认
s2占用内存大小为6字节
结论:结构体在对齐方式不合适的时候,我么可以自己更改默认对齐数。
结构体传参
直接上代码:
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; }
上面的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。(就是传递结构体的时候形参会临时拷贝一份该结构体,浪费了大量空间,传递地址效率更高)
结论:结构体传参的时候,要传结构体的地址。