结构填充是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字节),那么上述结构在内存中的表示如下图所示:

1.png

我们知道,结构体在内存中占用连续的空间,如上图所示,变量a占用1字节,变量b占用1字节,变量c占用4字节。在这种情况下,我们会遇到什么问题呢?

问题是什么?

由于我们考虑的是32位体系结构(每次读取4字节),所以一次只能读取4字节。问题是,变量a的1字节、变量b的1字节和变量c的2字节在一个CPU周期内可以被读取。我们读取char achar b变量时不会遇到任何问题,因为这两个变量可以在一个CPU周期内被读取。但是,当我们访问int c变量时会遇到问题,因为需要2个CPU周期才能读取完整的c变量值。在第一个CPU周期中,会读取前两个字节,而在第二个CPU周期中,会读取后两个字节。

假设我们只想访问c变量,而不访问ab变量,访问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字节,这已经在前面进行了讨论。

输出结果

2.png

改变变量的顺序

现在,我们来看看当改变变量的顺序时会发生什么,这是否会影响程序的输出。考虑下面的程序:

#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字节,如下面的截图所示。

输出结果

3.png

现在,我们需要理解为什么在这种情况下输出结果不同
4.png

  • 首先,内存被分配给变量a,即1字节。
    5.png
  • 然后,内存将被分配给变量b。由于int变量占用4字节,但左侧只有3字节可用。在这3字节上创建一个空行,使得int变量占用剩余的4字节,以便可以在一个CPU周期内访问整个整数变量。
    6.png
  • 最后,内存将被分配给变量c。每次CPU可以访问1个字,即4字节,所以CPU将使用4字节来访问c变量。因此,所需的总内存为12字节(4字节+4字节+4字节),即需要4字节来访问char a变量,4字节来访问int b变量,还需要4字节来访问char c变量的一个字符。
    6.png

如何避免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字节对齐。

输出结果

7.png

  • 使用属性
#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; 
}

输出结果

8.png

标签: c语言, c语言教程, c语言技术, c语言学习, c语言学习教程, c语言下载, c语言开发, c语言入门教程, c语言进阶教程, c语言高级教程, c语言面试题, c语言笔试题, c语言编程思想