linux中结构体对齐

本文详细解析了Linux中结构体的内存对齐原理及其重要性。通过对具体实例的剖析,阐述了内存对齐的三个基本原则,并通过代码示例展示了如何在实际编程中应用这些原则。

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

文章系转载,便于整理和分类,源文地址:https://blog.youkuaiyun.com/suifengpiao_2011/article/details/47260085

linux中定义对齐字节

typedef struct  sdk_handler {
	char comm_ver[10]; 
	char name[20];
	char reserve[20];
}PACKED  sdk_handler_t;

#define PACKED //__attribute__((aligned(1),packed))  // 一字节对齐

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

typedef struct MemAlign  
{  
    int a;  
    char b[3];  
    int c;  
}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字节对齐方式:

#pragma pack(4)  
typedef struct MemAlign  
{  
    char a[18];  
    double b;     
    char c;  
    int d;    
    short e;      
}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个字节来填充结构体,如下图红色的就是补齐出来的空间:

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

#include <stdio.h>  
#include <memory.h>  
  
// 由于VS2010默认是8字节对齐,我们  
// 通过预编译来通知编译器我们以4字节对齐  
#pragma pack(4)  
  
// 用于测试的结构体  
typedef struct MemAlign  
{  
    char a[18]; // 18 bytes  
    double b;   // 08 bytes   
    char c;     // 01 bytes  
    int d;      // 04 bytes  
    short e;    // 02 bytes  
}MemAlign;  
  
int main()  
{  
    // 定义一个结构体变量  
    MemAlign m;  
    // 定义个以指向结构体指针  
    MemAlign *p = &m;  
    // 依次对各个成员进行填充,这样我们可以  
    // 动态观察内存变化情况  
    memset( &m.a, 0x11, sizeof(m.a) );  
    memset( &m.b, 0x22, sizeof(m.b) );  
    memset( &m.c, 0x33, sizeof(m.c) );  
    memset( &m.d, 0x44, sizeof(m.d) );  
    memset( &m.e, 0x55, sizeof(m.e) );  
    // 由于有补齐原因,所以我们需要对整个  
    // 结构体进行填充,补齐对齐剩下的字节  
    // 以便我们可以观察到变化  
    memset( &m, 0x66, sizeof(m) );  
    // 输出结构体大小  
    printf( "sizeof(MemAlign) = %d", sizeof(m) );  
}  

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

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

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

补充:

1)对于结构体中有数组的,将数组作为基本数据类型来处理对齐;

2)对于结构体内嵌套结构体成员的,结构体成员的对齐和补齐原则:已最大成员作为内结构体整体的大小去匹配;

### C语言结构体对齐的原理和实现方式 #### 一、结构体对齐的基本概念 在C语言中,为了提高数据访问效率以及满足硬件架构的要求,编译器会对结构体中的成员进行字节对齐处理。这种对齐遵循一定的规则,主要包括以下几个方面: 1. **第一个成员的位置** 结构体的第一个成员始终存储在其分配空间的起始地址上[^1]。 2. **其他成员对齐规则** 其他成员会存储在一个称为“对齐数”的整数倍地址上。这个对齐数值取决于两个因素:一是该成员自身的大小;二是当前使用的编译器设置的默认对齐值。最终的实际对齐数是两者之间的较小值[^1]。 3. **不同平台下的差异** 编译器的不同可能导致对齐策略有所区别。例如,在Windows平台上使用Microsoft Visual Studio时,默认对齐数通常为8字节;而在Linux环境下GCC编译器则可能采用4字节作为默认对齐单位[^1]。 4. **整体大小调整** 整个结构体的总尺寸会被扩展到内部所有成员中最长对齐需求长度的一个整数倍。这意味着如果最后一个字段不需要额外填充来达到这一目标,则不会增加多余的空间消耗[^3]。 #### 二、具体实例分析 下面通过几个具体的代码片段进一步说明上述理论如何应用实践当中: ##### 示例1: 单纯字符型与整形组合情况 考虑以下简单定义: ```c #pragma pack(8) // 设置全局对齐参数为8字节 struct Example { char ch; int num; }; ``` 这里`ch`占据单个字节而`num`需要四个连续字节存放。由于我们指定了较大的打包指令(`pack=8`),所以即使只有这两个元素存在也至少得预留八位给整个对象[`sizeof(struct Example)`返回值应等于8][^2]. ##### 示例2: 复杂嵌套情形 再来看一段稍微复杂一点的例子涉及到了子结构体的情况: ```c #pragma pack(push, 8) typedef struct _s1{ short a; /* size = 2 */ long b; /* size = 4 or 8 depending on architecture*/ } S1; typedef struct _s2{ char c; /* size = 1 */ S1 d; /* contains 'a' and 'b', total aligned to max of them */ int e; /* size = 4 */ }s2; #pragma pack(pop) ``` 在这个场景里,S1类型的变量d本身又包含了多个属性因此它自己的布局也要考虑到各自的数据类型特性再加上外部施加的影响共同决定最后的结果即`s2`的整体规模应该是可以被最高级别也就是long或者double之类的大容量单元所接受的形式呈现出来[^4]. #### 三、总结 综上所述,C编程环境里的记录式复合数据形式——结构体会因为多种原因经历一系列复杂的排列过程从而形成特定模式下的物理表现形态这其中包括但不限于初始定位点选取原则、后续各项间间隔距离设定依据还有总体框架外围边界划定标准等等诸多环节相互作用最终达成既定目的同时兼顾性能优化考量. --- 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值