一. 使用sizeof 计算结构体的大小
通常情况下,我们习惯于使用 sizeof 运算符来计算结构体的大小。
例如,下面是一个结构体的定义:
struct Student {
int id;
char name[20];
int age;
float score;
};
其中,Student是该结构体的类型名,而id,name,age,score则是该结构体的成员。
接着我们在主函数内部创建一个结构体变量s。这时我们就可以使用sizeof运算符来计算这个结构体的大小了。如,直接使用sizeof操作符计算变量s的大小:
#include <stdio.h>
struct Student {
int id;
char name[20];
int age;
float score;
};
int main()
{
struct Student s;
printf("Size of struct Student is %d bytes\n", sizeof(s));
return 0;
}
运行结果为:
./test
Size of struct Student is 32 bytes
当然我们也可以不创建变量,直接将结构体类型放入sizeof中来计算该结构体类型的大小:
int main()
{
struct Student s;
printf("Size of struct Student is %d bytes\n", sizeof(struct Student));
return 0;
}
运行结果仍然是:
./test
Size of struct Student is 32 bytes
可以看到,这个结构体的大小是32个字节。
这是由于int类型占用4个字节,char类型占用1个字节,float类型占用4个字节,而且结构体中的成员顺序是按照定义的顺序来排列的。
因此我们似乎很容易就能计算出这个结果:4+20+4+4=32字节
但事实上 结构体的大小并不是通过这样简单累加计算的,如创建如下结构体:
struct stu
{
char ch1;
int i;
char ch2;
};
然后使用sizeof计算该结构体的大小
#include<stdio.h>
struct stu
{
char ch1;
int i;
char ch2;
};
int main()
{
printf("Size of struct stu is %d bytes\n", sizeof(struct stu));
return 0;
}
运行结果为:
./test123
Size of struct stu is 12 bytes
为什么是12,而不是 1+4+1=6?
别急,我们把结构体内部成员的顺序再调整一下:
struct stu
{
char ch1;
char ch2;
int i;
};
运行结果为:
./test123
Size of struct stu is 8 bytes
为什么是8 而不是12了呢?这两个结构体内部的成员没有改变,只是改变了位置,结果却不同。
通过以上测试,我们很容易发现,首先结构体的大小不是简单的每个成员大小逐个累加。其次,结构体的大小似乎和结构体成员的顺序也有关系。
那么结构体的大小到底是如何计算的呢?下面我们一起探究一下。
二. 影响结构体大小的因素
1. 结构体成员的类型
首先的影响因素就是结构体成员的类型,不同的结构体成员占用的内存大小不同。
如,一个int类型的成员占用4个字节,一个char类型的成员占用1个字节。
而C语言中常见的变量类型及其所占空间字节数如下表:
类型名 | 所占大小(单位:字节) |
---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4/8(取决于系统) |
float | 4 |
double | 8 |
long double | 16 |
2. 结构体成员的对齐方式
为了提高内存访问的效率,编译器会对结构体进行对齐。对齐的方式是按照成员的类型和顺序来进行的。
对齐的目的是为了让结构体成员的地址能够被整除,从而提高内存访问的速度。
3.结构体成员的顺序
结构体成员的顺序也会影响结构体的大小。
如果结构体成员的顺序不合理,可能会导致结构体的大小变得更大。
就像上面我们举的那个例子一样,结构体内部都是两个字符型数据和一个整形数据,但因为顺序不同,结构体的大小可能就完全不一样了。
三. 利用结构体对齐规律计算结构体大小
1. 结构体的对齐规则:
要知道结构体大小是如何计算的,首先需要了解结构体的对齐规则:
- 第一个成员在于结构体变量偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的一个对齐数(VS 中默认的为8) 与该成员大小的较小值。
- 结构体总大小为最大对齐数(每个成员变量都有自己的对齐数)的整数倍。
- 针对嵌套结构体,嵌套的结构体要对齐到自己最大对齐数的整数倍处,结构体总大小是所有对齐数的最大值(包含嵌套结构体的对齐数)的整数倍。
只看定义理解可能有些抽象,我们接下来画图举例说明一下这些对齐规则,如下面这个结构体:
struct stu
{
char ch1;
int i;
char ch2;
};
我们在前面运行过它的大小为12,而它的计算过程如下:
理解了这个结构体的大小是如何计算的,我们再来看看调整顺序后它为何又变成8了:
struct stu
{
char ch1;
char ch2;
int i;
};
理解了这两个结构体的内存大小是如何计算得出的,还有一种情况是当结构体中有成员是数组类型时,我们并不能将整个数组视为一整个成员,而是需要将数组中的元素拆开来继续一个一个对齐,直到排完最后一个数组元素为止。
如结构体中包含字符数组ch:
char ch[5];
在排列时就应该将该数组视为:
char ch1;
char ch2;
char ch3;
char ch4;
char ch5;
然后再将这些元素一一对齐在结构体即可。
2.结构体对齐的原因:
结构体对齐大致可以分为两个原因:
1> 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2> 性能原因:
内存对齐是指将变量存储在内存中时,按照一定的规则将变量的地址调整为某个特定值的过程。这个特定值通常是变量所占用的空间大小的整数倍。
结构体中的成员变量有可能会存在空洞,即某些成员变量之间的字节没有被使用。
这是因为编译器为了保证结构体成员变量的地址是按照一定规则对齐的,会在成员变量之间插入一些空字节。
这样做的好处是,可以提高程序的运行效率,因为当变量的地址按照一定规则对齐时,CPU可以更快地读取变量的值。
例如,一个结构体中包含一个char类型和一个int类型的成员变量,char 类型占用1个字节,int类型占用4个字节。如果不进行内存对齐,那么这个结构体的大小应该是5个字节,但是由于int类型的地址必须是4的倍数,因此编译器会在char 类型后面插入3个空字节,使得int类型的地址是4的倍数。这样,结构体的大小就变成了8个字节,其中3个字节是空洞。
图解如下:
3. 如何修改默认对齐数:
而有时我们会碰到结构体对齐方式不合适的时候,这时我们是可以自己修改系统默认对齐数的,如:
#include<stdio.h>
#pragma pack(2)
struct stu
{
char ch1;
int i;
char ch2;
};
int main()
{
printf("Size of struct stu is %d bytes\n", sizeof(struct stu));
return 0;
}
我们在一开始使用#pragma pack(2) 语句将系统默认对齐数改为了2.
修改后的运行结果则变成了:
./test123
Size of struct stu is 8 bytes
画图理解一下:
注意,当我们将默认对齐数改为1时,即:
#pragma pack(1)
则相当于没有对齐数,结构体的大小就是按顺序累加了,如:
./test123
Size of struct stu is 6 bytes
将默认对齐数改为1后,如上结构体的大小就变成1+4+1=6了。
在了解了结构体的对齐方式后,我们不仅可以准确的计算出结构体的大小,还可以依据对齐规则合理调整成员顺序,以减少结构体的内存浪费。同时可以通过修改对齐数来自由选择用空间换时间,还是用时间换空间。