先介绍三个概念:自身对齐值、指定对齐值、有效对齐值。
1、第一个成员在与结构体变量偏移量为0的地址处;
2、其他成员变量要对齐到某个数字(对齐数)的整数倍地址处;
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值;
3、结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍;
4、如果嵌套了结构体的情况嵌套结构体对齐到自己的最大对齐数的整数倍,结构体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
为什么存在内存对齐?
1、平台
自身对齐值:数据类型本身的对齐值,例如char类型的自身对齐值是1,short类型是2;
指定对齐值:编译器或程序员指定的对齐值,32位单片机的指定对齐值默认是4;
有效对齐值:自身对齐值和指定对齐值中较小的那个。
对齐有两个规则:
1、不但结构体的成员有有效对齐值,结构体本身也有对齐值,这主要是考虑结构体的数组,对于结构体或者类,要将其补齐为其有效对齐值的整数倍。结构体的有效对齐值是其最大数据成员的自身对齐值;
2、存放成员的起始地址必须是该成员有效对齐值的整数倍。
举四个例子
假如结构体起始地址是0x0000,
成员a的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0000是1的整数倍,故a存放起始地址是0x0000,占一个字节;
成员b的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0001是1的整数倍,故b存放起始地址是0x0001,占一个字节;
成员c的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0002是1的整数倍,故c存放起始地址是0x0002,占一个字节;
成员d的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0003是1的整数倍,故d存放起始地址是0x0003,占一个字节;
此时结构体A的有效对齐值是其最大数据成员的自身对齐值,它的成员都是char类型,故结构体A的有效对齐值是1.
结构体A的存储结构如下,其中Y是根据规则1补齐的字节,x是规则2补齐的字节。
0x0000 | 0x00001 | 0x0002 | 0x0003 |
a | b | c | d |
根据以上规则可以知道其他结构体的存储结构:
结构体B占6个字节
0x0000 | 0x00001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 |
a | x | b | b | c | d |
结构体C占12个字节
成员a的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0000是1的整数倍,故a存放起始地址是0x0000,占一个字节;
成员b的自身对齐值4,指定对齐值4,所以有效对齐值是4,地址0x0004是4的整数倍,故b存放起始地址是0x0004,占四个字节;
成员c的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0008是1的整数倍,故c存放起始地址是0x0008,占一个字节;
成员d的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0009是1的整数倍,故d存放起始地址是0x0009,占一个字节;
结构体C的成员占据10个字节,而结构体C的有效对齐值是其成员b的自身对齐值4,10不是4的倍数,故还需补齐两个字节,此时结构体C占据12个字节,是4的倍数
如下:
0x0000 | 0x00001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 | 0x0008 | 0x0009 | 0x000A | 0x000B |
a | x | x | x | b | b | b | b | c | d | Y | Y |
结构体D占16个字节
代码验证如下:
1、第一个成员在与结构体变量偏移量为0的地址处
2、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员大小的较小值。
VS中默认的值为8
3、结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
为什么存在内存对齐?
1、平台原因:不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;
2、性能原因:数据结构(尤其是栈)应尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问;32位的系统,有32根数据线,32根数据线,所以地址4字节,32位,读取数据也是一次性读取32位,4字节。64位系统就是地址是八个字节,一次读取64位。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
struct Test
{
int a;
char b;
int c;
} test;
理论上,结构体中的各个成员在内存中应该是连续储存的,就像数组里面的元素一样。事实上,也确实是这个样子的,不过和我们想象的有点不一样。
按照我们最初的想法,变量test所占的内存为 4 + 1 + 4 = 9。
但是我们写一个小代码验证一下发现和我们想的不一样。
它的内存为12。因为 int类型是4个字节,所以是不是各个成员的内存都是按照最大的那个设置呢?毕竟 4 * 3 = 12,我们再次实验,
如果按照我们的推测,那么内存大小应该是 8 * 3 = 24。为何是16呢?
下面我来总结一下。
总结
C语言结构体所占内存大小,其实里面涉及到C语言内存对齐,提高寻址效率的一种思想在里面。具体我就不在这里展开来说了,有兴趣的可以自己百度了解一下。
其实小可爱最想了解的应该是如何计算,结构体的内存大小。
不包含,数组和指针的结构体
对于不包含,数组和指针的结构体,知道各个成员所占内存大小后,可以直接相加,不过相加的时候必须保证前面的成员变量的内存所占内存必须是下一个成员变量所占内存的整倍数,如果不够就补上;并且最后的结果必须要是所占内存空间最大的成员变量的整倍数。
下面我来几个例子说明:
struct Test
{
double c; //8
int a; //4
char b; //1
} test;
所占内存大小,8 + 4 + 1 = 13,最大内存为8, 所以应该这样计算 8 + 4 + 4 = 16。
下面我们交换下,
struct Test
{
int a; //4
double c; //8
char b; //1
} test;
所占内存大小, 4 + 8 + 1 = 13,因为double类型是8个字节,而前面只有4个字节,并且成员变量最大内存为8,所以应该这样计算 8 + 8 + 8 = 24。
struct Test
{
int a; //4
char b; //1
double c; //8
} test;
同理4 + 1 + 8 = 13,应该变为 4 + 4 + 8 = 16。
包含,数组和指针的结构体
包含指针的结构体
对于包含指针的结构体,可以用和上面相同的方法进行计算,一般指针的大小都是固定的4个字节(在我的电脑上,你们可能不同),因为不管什么类型的指针只需要储存地址,不需要储存地址指向空间的内容。
struct Test
{
char a; //1
char *b; //4
double c; //8
} test;
struct Test
{
char a; //1
int *b; //4
double c; //8
} test;
struct Test
{
char a; //1
double *b; //4
double c; //8
} test;
这三种所占内存大小均为 4 + 4 + 8 = 16。如果将变量 b 和变量 c 的位置互换,则变为 8 + 8 + 8 = 24。
包含数组的结构体
数组中的元素地址是连续的,所以一个数组所占空间大小,为数组类型 * 元素个数。
知道了数组所占空间大小后,再来说说如何计算结构体中包含数组的情况,在之前计算的时候,我说过相加的时候必须保证前面的成员变量的内存所占内存必须是下一个成员变量所占内存的整倍数,但是如果下一变量为数组,则没有这个要求。
例如:
struct Test
{
int a;
char b[21];
int d;
double c;
} test;
应该为 4 + 24 + 4 + 8 = 40。
struct Test
{
int a;
char b[19];
int d;
double c;
} test;
应该为 4 + 20 + 8 + 8 = 40。
struct Test
{
char a; //1
char b[19]; //19
int d; //4
double c; //8
} test;
应该为 1 + 19 + 4 + 8 = 32
struct Test
{
char a; //1
char b[17]; //17
int d; //4
double c; //8
} test;
应该为 1 + 19 + 4 + 8 = 32。
struct Test
{
char a; //1
char b[15]; //15
int d; //4
double c; //8
} test;
应该为 1 + 15 + 8 + 8 = 32。