一.计算结构体的大小
要计算结构体的大小,首先我们要清楚结构体在内存中是如何存储的
结构体的大小计算遵循结构体的对齐规则:
1.第一个成员在与结构体变量偏移量为0的地址处。(即结构体的首地址处,即对齐到0处)
2.其他成员变量要对齐到此成员变量本身的对齐数的整数倍的地址处。
3.结构体的总大小为结构体内所有成员中最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体,嵌套的结构体对齐到其成员中最大对齐数的整数倍处。
对齐数 = 该结构体成员变量自身的大小与编译器默认的一个对齐数的较小值。
注:VS中的默认对齐数为8,不是所有编译器都有默认对齐数,当编译器没有默认对齐数的时候,成员变量的大小就是该成员的对齐数。(下列示例中默认对齐数为8)
如图1:
在上图中我们定义了一个结构体定义S1,
首先系统会将字符型变量a存入第0个字节(相对地址,指内存开辟的首地址);
然后在存放整形变量b时,会以4个字节为单位进行存储,由于第一个四字节模块已有数据,因此它会存入第二个四字节模块,也就是存入到4~7字节;同理,存放双精度实型变量c时,由于其宽度为8,其存放时会以8个字节为单位存储,也就是会找到第一个空的且是8的整数倍的位置开始存储,此例中,此例中,由于头一个8字节模块已被占用,所以将c存入第二个8字节模块,即存入到4~7字节。整体存储示意图如图1所示。
接下来我们看第二个例子
在例二中仅仅是将double型的变量和int型的变量互换了位置。测试程序不变,测试结果却截然不同,sizeof(S2)=24,这里就涉及到了对齐规则中的第三条。
在经过分析后,检查计算出的存储单元是否为结构体的总大小为结构体内所有成员中最大对齐数(每个成员变量都有一个对齐数)的整数倍。是,则结束;若不是,则补齐为它的整数倍。
我们再来分析一个结构体嵌套的例子:
这里涉及到了对齐规则中的第四条。
经测试,可知sizeof(X)为16,sizeof(Y)为24。即计算Y的存储长度时,在存放第二个元素b时的初始位置是在double型的长度8的整数倍处,而非16的整数倍处,即系统为b所分配的存储空间是第8~23个字节。
另外注意当有结构体中包含数组成员时,每个数组元素都占有和其元素类型相对的对齐数,如:char[5],即连续存储五个char类型的元素。
为什么存在内存对齐?
平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。
比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。
性能原因: 数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐内存,处理器需要作两次内存访问;而对齐的内存访问仅需一次。
在画图时可能有博友会想,内存这么重要,在进行内存对齐的时候怎么还有内存被白白浪费掉呢?
现在看来,其实结构体的内存对齐是拿空间来换取时间的做法。
设计结构体时的技巧
其实在我们设计结构体的时候,如果结构体成员的顺序设计得合理的话,是可以避免不必要的内存消耗的。
两个结构体的成员变量相同,但是成员变量的顺序不同,可能就会出现结构体的大小不同的情况:
struct S1
{
char a;
char b;
int c;
};//结构体1
struct S2
{
char a;
int c;
char b;
};//结构体2
我们可以看到,结构体1和结构体2的成员变量一模一样,可是当我们按照内存对齐规则来计算两个结构体的大小的时候,会发现两个结构体的大小不一样,在VS编译器下第一个结构体大小为8,第二个结构体大小为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;
}
于是,当结构体的对齐方式不合适的时候,我们可以自己更改默认对齐数。
二.计算位段的大小
位段和结构体很像,但存储时却不同
C语言中没有专门的位段类型,位段的定义要借助于结构体,即以二进制位为单位定义结构体成员所占存储空间。
例如:
在vs环境下位段的存储方式:
//下面存储采用二进制表示
struct S
{
char a : 3;//这里开辟的一个字节存储的内容为00 00 10 10;
//因为只要了3个bite位的存储空间所以截断后实际存储内容只有010;
//第一个字节还剩5个bite位没用
char b : 4;//这里一个字节存储的内容为00 00 11 00;
//因为只要了4个bite位的存储空间所以实际存储内容只有1100;
//因为第一个字节还剩5个bite位,而存储b只需4个bite位(4<5),所以b和a
//共用第一个字节
//加上前面存储的a的内容为1100010,共7个bite位,还剩1个bite位
char c : 5;//这里c要了5个bite位,因为(5>1),则新开辟一个字节
//存储的内容为00 00 00 11;
//因为只要了三个bite位的存储空间所以实际存储内容只有00011;剩下3个bite位
char d : 4;//同理(4>3),则需要新开辟一个字节,
//存储的内容为00 00 01 00;
//因为只要了四个bite位的存储空间所以实际存储内容只有0100;剩下4个bite位
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(s));//这里计算得三个字节
return 0;
需要注意的是,位段在不同编译器上的存储方式都有不同,所以尽量避免跨平台移植的使用。
谢谢阅读,希望对大家能有所帮助。