C语言教程-C语言中的结构填充

结构填充是C语言中的一个概念,通过在内存地址之间添加一个或多个空字节,来对齐数据在内存中的存储方式。
让我们通过下面的一个简单场景来理解C语言中的结构填充:
假设我们创建了一个自定义结构。当我们创建该结构的对象时,内存会按照结构成员的顺序分配连续的空间。
struct student
{
char a;
char b;
int c;
} stud1;
在上述示例中,我们创建了一个名为student
的结构。我们声明了一个名为stud1
的该结构的对象。在创建对象后,结构的各个成员会在内存中连续分配空间。首先,内存将被分配给变量a
,然后是变量b
,最后是变量c
。
结构体student
的大小是多少?
现在,我们来计算结构体student
的大小。我们假设int
类型的大小为4字节,char
类型的大小为1字节。
struct student
{
char a; // 1字节
char b; // 1字节
int c; // 4字节
};
在上述情况下,当我们计算结构体student
的大小时,结果为6字节。但是这个答案是错误的。现在,我们将了解为什么这个答案是错误的,我们需要理解结构填充的概念。
结构填充
处理器并不是逐个字节地读取数据,而是以一个字(word)为单位进行读取。
什么是一个字(word)?
如果我们使用32位处理器,处理器每次读取4字节,即一个字等于4字节。
1个字 = 4字节
如果我们使用64位处理器,处理器每次读取8字节,即一个字等于8字节。
1个字 = 8字节
因此,我们可以说,32位处理器一次可以读取4字节,64位处理器一次可以读取8字节。这取决于体系结构中字的大小。
为什么需要结构填充?
如果我们使用32位处理器(每次读取4字节),那么上述结构在内存中的表示如下图所示:
我们知道,结构体在内存中占用连续的空间,如上图所示,变量a
占用1字节,变量b
占用1字节,变量c
占用4字节。在这种情况下,我们会遇到什么问题呢?
问题是什么?
由于我们考虑的是32位体系结构(每次读取4字节),所以一次只能读取4字节。问题是,变量a
的1字节、变量b
的1字节和变量c
的2字节在一个CPU周期内可以被读取。我们读取char a
和char b
变量时不会遇到任何问题,因为这两个变量可以在一个CPU周期内被读取。但是,当我们访问int c
变量时会遇到问题,因为需要2个CPU周期才能读取完整的c
变量值。在第一个CPU周期中,会读取前两个字节,而在第二个CPU周期中,会读取后两个字节。
假设我们只想访问c
变量,而不访问a
和b
变量,访问c
变量需要两个周期。c
变量占用4字节,所以也可以在一个周期内访问完。但在这种情况下,却使用了两个周期。这是对CPU周期的不必要浪费。因此,引入了结构填充的概念,以节省CPU周期。结构填充由编译器自动完成。接下来,我们将看看结构填充是如何进行的。
结构填充是如何进行的?
为了实现结构填充,在上图中,会在左侧创建一个空行,并将左侧被c
变量占用的两个字节向右移动。这样,c
变量的所有四个字节都位于右侧。现在,c
变量可以在一个CPU周期内访问。结构填充后,结构体占用的总内存为8字节(1字节+1字节+2字节+4字节),比之前的内存占用更多。虽然在这种情况下浪费了一些内存,但变量可以在一个周期内访问。
下面是一个创建结构体的简单程序示例:
#include <stdio.h>
struct student
{
char a;
char b;
int c;
};
int main()
{
struct student stud1; // 声明类型为student的变量
// 显示结构体student的大小
printf("The size of the student structure is %d", sizeof(stud1));
return 0;
}
在上述代码中,我们创建了一个名为student
的结构体。在main()
函数内部,我们声明了一个类型为student
的变量stud1
,然后使用sizeof()
运算符计算了student
的大小。由于存在结构填充的概念,所以输出结果为8字节,这已经在前面进行了讨论。
输出结果
改变变量的顺序
现在,我们来看看当改变变量的顺序时会发生什么,这是否会影响程序的输出。考虑下面的程序:
#include <stdio.h>
struct student
{
char a;
int b;
char c;
};
int main()
{
struct student stud1; // 声明类型为student的变量
// 显示结构体student的大小
printf("The size of the student structure is %d", sizeof(stud1));
return 0;
}
上述代码与前面的代码相似,唯一的改变是structure student
中变量的顺序。由于顺序的改变,两种情况下的输出将不同。在前面的情况下,输出结果为8字节,但在此情况下,输出结果为12字节,如下面的截图所示。
输出结果
现在,我们需要理解为什么在这种情况下输出结果不同。
- 首先,内存被分配给变量
a
,即1字节。 - 然后,内存将被分配给变量
b
。由于int
变量占用4字节,但左侧只有3字节可用。在这3字节上创建一个空行,使得int
变量占用剩余的4字节,以便可以在一个CPU周期内访问整个整数变量。 - 最后,内存将被分配给变量
c
。每次CPU可以访问1个字,即4字节,所以CPU将使用4字节来访问c
变量。因此,所需的总内存为12字节(4字节+4字节+4字节),即需要4字节来访问char a
变量,4字节来访问int b
变量,还需要4字节来访问char c
变量的一个字符。
如何避免C语言中的结构填充?
结构填充是编译器自动完成的内置过程。有时需要避免结构填充,因为它会使结构体的大小大于结构体成员的大小。
我们可以通过两种方式避免C语言中的结构填充:
- 使用
#pragma pack(1)
指令 - 使用属性
使用#pragma pack(1)
指令
#include <stdio.h>
#pragma pack(1)
struct base
{
int a;
char b;
double c;
};
int main()
{
struct base var; // 声明类型为base的变量
// 显示结构体base的大小
printf("The size of the var is : %d", sizeof(var));
return 0;
}
在上面的代码中,我们使用#pragma pack(1)
指令来避免结构填充。如果我们不使用该指令,那么上述程序的输出结果将为16字节。但实际上结构体成员的大小为13字节,所以有3字节浪费掉了。为了避免内存浪费,我们使用#pragma pack(1)
指令提供1字节对齐。
输出结果
- 使用属性
#include <stdio.h>
struct base
{
int a;
char b;
double c;
}__attribute__((packed));
int main()
{
struct base var; // 声明类型为base的变量
// 显示结构体base的大小
printf("The size of the var is : %d", sizeof(var));
return 0;
}
输出结果