结构体补齐与对齐

本文详细解析了C语言中结构体的内存对齐原理,包括对齐的基本原则及其实现过程,并通过实例演示了如何计算结构体的实际内存占用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文地址:

首先我们先看看下面的C语言的结构体:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. typedef struct MemAlign  
  2. {  
  3.     int a;  
  4.     char b[3];  
  5.     int c;  
  6. }MemAlign;  

    以上这个结构体占用内存多少空间呢?也许你会说,这个简单,计算每个类型的大小,将它们相加就行了,以32为平台为例,int类型占4字节,char占用1字节,所以:4 + 3 + 4 = 11,那么这个结构体一共占用11字节空间。好吧,那么我们就用实践来证明是否正确,我们用sizeof运算符来求出这个结构体占用内存空间大小,sizeof(MemAlign),出乎意料的是,结果居然为12?看来我们错了?当然不是,而是这个结构体被优化了,这个优化有个另外一个名字叫“对齐”,那么这个对齐到底做了什么样的优化呢,听我慢慢解释,再解释之前我们先看一个图,图如下:

    相信学过汇编的朋友都很熟悉这张图,这张图就是CPU与内存如何进行数据交换的模型,其中,左边蓝色的方框是CPU,右边绿色的方框是内存,内存上面的0~3是内存地址。这里我们这张图是以32位CPU作为代表,我们都知道,32位CPU是以双字(DWORD)为单位进行数据传输的,也正因为这点,造成了另外一个问题,那么这个问题是什么呢?这个问题就是,既然32位CPU以双字进行数据传输,那么,如果我们的数据只有8位或16位数据的时候,是不是CPU就按照我们数据的位数来进行数据传输呢?其答案是否定的,如果这样会使得CPU硬件变的更复杂,所以32位CPU传输数据无论是8位或16位都是以双字进行数据传输。那么也罢,8位或16位一样可以传输,但是,事情并非像我们想象的那么简单,比如,一个int类型4字节的数据如果放在上图内存地址1开始的位置,那么这个数据占用的内存地址为1~4,那么这个数据就被分为了2个部分,一个部分在地址0~3中,另外一部分在地址4~7中,又由于32位CPU以双字进行传输,所以,CPU会分2次进行读取,一次先读取地址0~3中内容,再一次读取地址4~7中数据,最后CPU提取并组合出正确的int类型数据,舍弃掉无关数据。那么反过来,如果我们把这个int类型4字节的数据放在上图从地址0开始的位置会怎样呢?读到这里,也许你明白了,CPU只要进行一次读取就可以得到这个int类型数据了。没错,就是这样,这次CPU只用了一个周期就得到了数据,由此可见,对内存数据的摆放是多么重要啊,摆放正确位置可以减少CPU的使用资源。

那么,内存对齐有哪些原则呢?我总结了一下大致分为三条:
第一条:第一个成员的首地址为0
第二条:每个成员的首地址是自身大小的整数倍
       第二条补充:以4字节对齐为例,如果自身大小大于4字节,都以4字节整数倍为基准对齐。
第三条:最后以结构总体对齐。
        第三条补充:以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。(其中这一条还有个名字叫:“补齐”,补齐的目的就是多个结构变量挨着摆放的时候也满足对齐的要求。)

    上述的三原则听起来还是比较抽象,那么接下来我们通过一个例子来加深对内存对齐概念的理解,下面是一个结构体,我们动手算出下面结构体一共占用多少内存?假设我们以32位平台并且以4字节对齐方式:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #pragma pack(4)  
  2. typedef struct MemAlign  
  3. {  
  4.     char a[18];  
  5.     double b;     
  6.     char c;  
  7.     int d;    
  8.     short e;      
  9. }MemAlign;  
下图为对齐后结构如下:

我们就以这个图来讲解是如何对齐的:
第一个成员(char a[18]):首先,假设我们把它放到内存开始地址为0的位置,由于第一个成员占18个字节,所以第一个成员占用内存地址范围为0~18。
第二个成员(double b):由于double类型占8字节,又因为8字节大于4字节,所以就以4字节对齐为基准。由于第一个成员结束地址为18,那么地址18并不是4的整数倍,我们需要再加2个字节,也就是从地址20开始摆放第二个成员。
第三个成员(char c):由于char类型占1字节,任意地址是1字节的整数倍,所以我们就直接将其摆放到紧接第二个成员之后即可。
第四个成员(int d):由于int类型占4字节,但是地址29并不是4的整数倍,所以我们需要再加3个字节,也就是从地址32开始摆放这个成员。
第五个成员(short e):由于short类型占2字节,地址36正好是2的整数倍,这样我们就可以直接摆放,无需填充字节,紧跟其后即可。
    这样我们内存对齐就完成了。但是离成功还差那么一步,那是什么呢?对,是对整个结构体补齐,接下来我们就补齐整个结构体。那么,先让我们回顾一下补齐的原则:“以4字节对齐为例,取结构体中最大成员类型倍数,如果超过4字节,都以4字节整数倍为基准对齐。”在这个结构体中最大类型为double类型(占8字节),又由于8字节大于4字 节,所以我们还是以4字节补齐为基准,整个结构体结束地址为38,而地址38并不是4的整数倍,所以我们还需要加额外2个字节来填充结构体,如下图红色的就是补齐出来的空间:

到此为止,我们内存对齐与补齐就完毕了!接下来我们用实验来证明真理,程序如下:

[cpp]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. #include <stdio.h>  
  2. #include <memory.h>  
  3.   
  4. // 由于VS2010默认是8字节对齐,我们  
  5. // 通过预编译来通知编译器我们以4字节对齐  
  6. #pragma pack(4)  
  7.   
  8. // 用于测试的结构体  
  9. typedef struct MemAlign  
  10. {  
  11.     char a[18]; // 18 bytes  
  12.     double b;   // 08 bytes   
  13.     char c;     // 01 bytes  
  14.     int d;      // 04 bytes  
  15.     short e;    // 02 bytes  
  16. }MemAlign;  
  17.   
  18. int main()  
  19. {  
  20.     // 定义一个结构体变量  
  21.     MemAlign m;  
  22.     // 定义个以指向结构体指针  
  23.     MemAlign *p = &m;  
  24.     // 依次对各个成员进行填充,这样我们可以  
  25.     // 动态观察内存变化情况  
  26.     memset( &m.a, 0x11, sizeof(m.a) );  
  27.     memset( &m.b, 0x22, sizeof(m.b) );  
  28.     memset( &m.c, 0x33, sizeof(m.c) );  
  29.     memset( &m.d, 0x44, sizeof(m.d) );  
  30.     memset( &m.e, 0x55, sizeof(m.e) );  
  31.     // 由于有补齐原因,所以我们需要对整个  
  32.     // 结构体进行填充,补齐对齐剩下的字节  
  33.     // 以便我们可以观察到变化  
  34.     memset( &m, 0x66, sizeof(m) );  
  35.     // 输出结构体大小  
  36.     printf( "sizeof(MemAlign) = %d"sizeof(m) );  
  37. }  

程序运行过程中,查看内存如下:

其中,各种颜色带下划线的代表各个成员变量,蓝色方框的代表为内存对齐时候填补的多余字节,由于这里看不到补齐效果,我们接下来看下图,下图篮框包围的字节就是与上图的交集以外的部分就是补齐所填充的字节。

在最后,我在谈一谈关于补齐的作用,补齐其实就是为了让这个结构体定义的数组变量时候,数组内部,也同样满足内存对齐的要求,为了更好的理解这点,我做了一个跟本例子相对照的图:

### C语言结构体对齐补齐规则 #### 结构体成员排列原则 在C语言中,编译器按照特定规则来安排结构体成员的位置。通常情况下,编译器会遵循以下两个基本原则: - **按声明顺序依次分配空间**:即先为第一个字段分配所需的空间,再接着为第二个字段分配其所需的连续存储区域,依此类推。 - **满足数据类型的自然边界要求**:每种基本数据类型都有自己的“自然边界”,指该类型变量地址相对于起始位置应能被多少整除。 对于不同平台上的具体数值大小差异,这里主要讨论常见的32位系统环境下的情况[^1]。 #### 自然边界填充字节 为了提高CPU访问效率,某些架构偏好让不同类型的数据位于特定偏移量处。例如,在大多数现代计算机体系结构上: - `char` 类型占用1个字节; - `short` 占用2个字节; - `int`, `float` 各自占据4个字节; - 而像 `double` 这样的双精度浮点数则需8个字节。 当创建包含多种长度不同的成员项的复合对象(如结构体)时,如果直接依照上述简单方式布局,则可能导致浪费大量零散的小块未使用的内存片段—这些被称为“孔洞”。为了避免这种情况发生并优化性能表现,编译器会在必要时候自动插入额外的无意义填充字节以确保后续元素能够正确地落在它们各自期望的地址之上[^2]。 #### 控制对齐属性的方法 有时开发者希望改变默认行为以便更好地控制程序运行期间所消耗的实际物理资源数量或是出于兼容性的考虑。为此提供了几种机制可供选择调整内部组件间的相对定位关系: ##### 使用预处理器指令 #pragma pack() 通过设置参数n可指定全局范围内所有新定义出来的struct实例都采用固定宽度作为最小单位进行打包处理。比如下面的例子就强制设定了最大不超过四个字节间隔度量标准[^3]: ```cpp #pragma pack(push, 4) /* 开启新的pack作用域 */ typedef struct { char ch; double db; } MyStruct; /* ...其他代码... */ #pragma pack(pop) /* 关闭当前最近一次push的效果 */ ``` ##### 应用编译选项或命令行开关 部分工具链允许经由外部配置文件或者构建脚本传递特殊标记告知链接阶段如何操作目标二进制文件中的section alignment信息。不过这类做法往往依赖于具体的开发套件版本以及宿主机操作系统特性等因素而有所区别[^4]. #### 示例分析 考虑到以上知识点,现在来看一段实际例子及其对应的内存映射图解说明: ```c #include <stdio.h> // 定义一个简单的嵌套结构体 typedef struct inner_s{ char s1; // 1 byte + 7 bytes padding (to align next member on an 8-byte boundary) double s2; // 8 bytes char s3; // 1 byte + 7 bytes padding (for structure size to be multiple of largest member's alignment requirement which is 8 here) }inner_t; typedef struct outer_s{ char t1; // 1 byte + 3 bytes padding (aligning the following 't2' field at its natural boundary i.e., 4-bytes aligned since it contains another struct whose biggest element has this property.) inner_t t2; // As calculated above -> total 16 bytes. double t3; // Directly follows without any gap because both have same alignment needs. }outer_t; int main(){ printf("Sizeof(inner): %zu\n", sizeof(inner_t)); printf("Sizeof(outer): %zu\n", sizeof(outer_t)); return 0; } ``` 输出结果将是: ``` Sizeof(inner): 24 Sizeof(outer): 32 ``` 这是因为即使看似只有三个字符加上一个双精度实数,但由于存在隐式的填充使得最终尺寸远大于直观感受。特别是外层结构体还包含了内层的一个副本,因此整体增长更为显著.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值