C语言 计算结构体大小
前言数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。如何计算结构体大小以下面这个结构体为例:struct S{double d;char c;int i;};
本文主要参考:结构体内存对齐(如何计算结构体的大小)
前言
数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。
结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。
如何计算结构体大小
以下面这个结构体为例:
struct S
{
double d;
char c;
int i;
};
使用 sizeof(struct S) 计算其大小发现结果是16,并不是double 8字节+char 1字节+int 4字节=13字节。
下面分3步分析该结构体的大小:
第一步:找出每个成员变量的大小将其与编译器的默认对齐数相比较,取其较小值为该成员变量的对齐数。
【注:使用VS编译运行,故默认对齐数为8。】
第二步:根据每个成员对应的对齐数画出它们在内存中的相对位置。
第三步:通过最大对齐数决定最终该结构体的大小。
通过图我们可以知道,绿色部分(double d成员占用)+红色部分(char c成员占用)+紫色部分(int i成员占用)+红色与紫色之间的白色部分(浪费掉了)总共占用了16个字节的内存空间。
我们需要将它们总共占用的内存空间(16)与结构体成员的最大对齐数(8)相比较,结构体的总大小为最大对齐数的整数倍,此时16正好是8的整数倍,所以该结构体在VS编译器下的大小就16个字节。即创建一个该类型的结构体变量,内存需为其开辟16个字节的内存空间。
注意:大多数情况下,成员变量已经占用的总字节个数并不一定正好为其成员变量中的最大对齐数的整数倍,这时我们需要将其扩大为最大对齐数的整数倍。
代码验证上述分析:
#include <stdio.h>
struct S
{
double d;
char c;
int i;
};
int main(void)
{
struct S test;
printf("%d\n",sizeof(struct S));
printf("%p\n",&test);
printf("%p\n", &test.d);
printf("%p\n", &test.c);
printf("%p\n", &test.i);
return 0;
}
结果:
由该验证结果可见:
1)该结构体一共16个字节;
2)第一个成员d是从结构体的初始地址开始存储的;
3)第二个成员c相对第一个成员的地址偏移了8字节;
4)第三个成员i相对第二个成员的地址偏移了4字节。
因此是符合上面的图表所示的。
结构体大小占用规则
1、第一个成员在与结构体变量偏移量为0的地址处。(即结构体的首地址处,即对齐到0处)
2、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
3、结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4、如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对齐数 = 该结构体成员变量自身的大小与编译器默认的一个对齐数的较小值。
注:VS中的默认对齐数为8,不是所有编译器都有默认对齐数,当编译器没有默认对齐数的时候,成员变量的大小就是该成员的对齐数。
计算举例
1、简单结构体
struct s1{
char ch1;//1
char ch2;//1
int i;//空2 + 4 = 6
};
这个结构体的大小容易计算,为8,那么下面这个呢
struct s2{
char ch1;//1(字节)
int i; //空3 + 4 = 7
char ch2;//1 + 空3
};
这个结构体大小是12字节,为什么呢?
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。因此第二个成员的对齐数是4,相对首地址偏移4字节,与第一个成员之间空了3个字节,第三个成员的对齐数是1,相对首地址偏移5字节(紧随第二个成员)。
结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。最大对齐数是4,如果只是1+空3+4+1=9还不够最大对齐数,因此后面还需补齐3个空字节。所以总大小为12字节。
2、成员包含数组的结构体
struct s3{
char ch;//1(字节)
int i; //空3 + 4 = 7(字节)
char str[10];//10 + 空2 = 12
};
这个结构体的大小是20,先看前两个成员,大小是8,这个char类型的数组,只需要把它看做十个char型连在一起即可,加起来就是18,再满足结构体总大小为最大对齐数的整数倍,最大对齐数是第二个成员对应的4,数组应该拆分来看,不能当整体看,所以总大小就是20。其中包含5个空字节。
3、成员包含结构体的结构体
struct s4{
char ch;//1
int i;//空3 + 4 = 7
struct s{
char ch1; //1
int j; //空3 + 4 = 7
};
float f;//4
};
里面这个结构体的大小是8,那么是否结构体大小就要向8对齐呢?这个结构体的大小是20,很明显不是8的倍数。所以计算结构体大小时是把里面这个结构体就看做是一个char,和一个int,不是看做一个整体。
扩展:
上面这种写法,中间嵌套的结构体没有变量名,访问方式里面的成员如下:
struct s4 test;
test.ch1 = 0;
test.j = 0;
可以改成:
struct s4{
char ch;//1
int i;//空3 + 4 = 7
struct s{
char ch1; //1
int j; //空3 + 4 = 7
}S;
float f;//4
};
中间嵌套的结构体有变量名了,访问方式里面的成员如下:
struct s4 test;
test.S.ch1 = 0;
test.S.j = 0;
但上面两种写法的结构体总大小都是20。
4、成员包含联合体的结构体
struct s5{
char ch;//1
int i;//空3+4=7
union{
char ch1;
int j;//4
};
};
联合体大小就是成员中最大类型的大小,所以这个结构体大小是12.
修改默认对齐数
要修改编译器的默认对齐数,我们需要借助于以下预处理命令:
#pragma pack()
如果在该预处理命令的括号内填上数字,那么默认对齐数将会被改为对应数字;如果只使用该预处理命令,不在括号内填写数字,那么会恢复为编译器默认的对齐数。
#include <stdio.h>
#pragma pack(4)//设置默认对齐数为4
struct S1
{
char a;//1/4->1
int b;//4/4->4
char c;//1/4->1
};//12
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char a;//1/1->1
int b;//4/1->1
char c;//1/1->1
};//6
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S1));//打印结果为12
printf("%d\n", sizeof(struct S2));//打印结果为6
return 0;
}
【关于#pragma pack()的作用域】
#pragma pack(n)是#pragma编译预处理指令最基本的用法,其作用是改变编译器的对齐方式,n值可以取(1, 2, 4, 8, 16) 中任意一值。
若存在多个#pragma pack (n),遵从向上对齐原则,即某个结构体定义上方最近的一个#pragma pack()
#pragma pack()下方所有的代码,对齐原则都变更成设置的值。
如果放在头文件里,所有包含了该头文件的C文件都按此设置的对齐值编译执行。
https://www.jianshu.com/p/90a6eef329ec
思考:为什么存在内存对齐?
平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。
比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。
性能原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次。因此在某些情况下需要在成员间或是末尾补齐空字节,以空间换时间。
设计结构体时的技巧
其实在我们设计结构体的时候,如果结构体成员的顺序设计得合理的话,是可以避免不必要的内存消耗的。
两个结构体的成员变量相同,但是成员变量的顺序不同,可能就会出现结构体的大小不同的情况:
struct S1
{
char a;
char b;
int c;
};//结构体1
struct S2
{
char a;
int c;
char b;
};//结构体2
我们可以看到,结构体1和结构体2的成员变量一模一样,可是当我们按照内存对齐规则来计算两个结构体的大小的时候,会发现两个结构体的大小不一样,在VS编译器下第一个结构体大小为8,第二个结构体大小为12。
可以见得,结构体成员变量的顺序不同,可能会造成内存不必要的损失。将占用空间小的成员尽量集中在一起,可以有效地避免内存不必要的浪费。
结语
在其他文章中看过对结构体大小计算规则的一些概括,都是不太严谨或是有错误的;
比如:
1,每个结构体成员的起始地址为该成员大小的整数倍,即int型成员的起始地址只能为0、4、8等
2,结构体的大小为其中最大成员大小的整数倍
反例:
struct s3{
char ch;//1(字节)
int i; //空3 + 4 = 7(字节)
char str[11];//11 + 空1 = 12
};
这个结构体的大小是20,第2规则 结构体的大小为其中最大成员大小的整数倍,这里面最大成员是str数组为11个字节,20并不是11的整数倍。应该理解成:结构体的总大小为最大对齐数的整数倍。最大对齐数是第二个成员对应的4。
又比如:
一、结构体成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
二、结构体大小必须是所有成员大小的整数倍
以上都没有考虑修改默认对齐数的情况:
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char a;//1/1->1
int b;//4/1->1
char c;//1/1->1
};//6
#pragma pack()//取消设置的默认对齐数,还原为默认
该结构体大小为6,显然不是第二个成员大小4的整数倍。
参考鸣谢:
结构体内存对齐(如何计算结构体的大小)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)