一文搞懂c语言字节对齐
如果你对c语言的字节对齐总感觉模糊,不能从理论上推导出一个复杂结构体实际占用的内存大小,那么你必须要看看本博文,硬干货!!!
测试环境是ubuntu 64位
基本概念
要透彻理解本文,需要先知道三个概念:自身对齐,默认对齐,有效对齐
自身对齐
结构体的成员的自身大小对齐
char a; //1字节
short b; //2字节
int c; //4字节
float d; //4字节
double e; //8字节
默认对齐
32位系统一般为4字节对齐,64位系统一般为8字节对齐
我的服务器内存的数据位宽位64bits,也就是8字节。
有效对齐
min{自身对齐,默认对齐}.即自身对齐和默认对齐的较小值.
对齐规则
网上有很多文章,给出了很多对齐规则,我觉得太繁杂,我这里就一条规则
按有效对齐分配内存
接下来,通过一个详细的例子来分析 复杂结构体的大小分配以及每个成员的对齐分布
typedef struct _d_s
{
char a; //1字节
short b; //2字节
int c; //4字节
float d; //4字节
}d_t; //12字节, 有效对齐4字节
// 如果不考虑对齐因素, d_t的原始大小为11字节;但是该结构体中,有效对齐为4字节,那么d_t的实际大小为12字节
typedef char string[31];
typedef struct _g_s
{
char a; //1字节
short b; //2字节
int c; //4字节
float d; //4字节
double e; //8字节
d_t f; //12字节
char x; //1字节
string g; //31字节
short h; //2字节
int i; //4字节
}g_t; //默认8字节对齐;1+2+4+4+8+12+1+31+2+4=69
//下面为g_t变量的实际大小及内存地址分布
printf("size of g_t:%ld\n", sizeof(g_t));
g_t g;
printf("address of g.a:%p\n", &g.a);
printf("address of g.b:%p\n", &g.b);
printf("address of g.c:%p\n", &g.c);
printf("address of g.d:%p\n", &g.d);
printf("address of g.e:%p\n", &g.e);
printf("address of g.f:%p\n", &g.f);
printf("address of g.x:%p\n", &g.x);
printf("address of g.g:%p\n", &g.g);
printf("address of g.h:%p\n", &g.h);
printf("address of g.i:%p\n", &g.i);
我们先看看实际运行时,g_t变量所占内存大小及地址分布情况:
这里有个疑问,有效对齐为8字节,而g_t的原始大小为69字节,理论上g_t的实际大小为72字节应该是足够存储,但为什么实际大小是80字节呢?
上图中,每一个小格代表一个字节。每8个字节看作一个默认对齐单元
。
已知g.a的起始地址为0x7ffc425478e0
(每次运行地址都会变,以自己实际运行地址为准,但地址一定是8字节对齐),g.a占一个字节;
由于g.b占2字节,g.b的有效对齐为2字节,所以g.b的起始地址为0x7ffc425478e2
,那么,0x7ffc425478e1
为填充字节;
由于g.c占4字节,g.c的有效对齐为4字节,刚好0x7ffc425478e0
代表的默认对齐单元
能够存放下g.c,所以g.c的起始地址为0x7ffc425478e4
;
由于g.d占4字节,g.d的有效对齐为4字节,所以g.d的起始地址为0x7ffc425478e8
;
由于g.e占8字节,g.e的有效对齐为8字节,!!!重点部分,注意理解!!! 但是0x7ffc425478e8
所在的默认对齐单元
只剩下4个字节,起始地址为0x7ffc425478ec
,显然该地址0x7ffc425478ec
不是8字节对齐,故g.e不能存放在0x7ffc425478e8
所在的默认对齐单元
了;既然不能放,那就只能往后再开辟一个新的默认对齐单元
,即0x7ffc425478f0
,且刚好放满。
由于g.f占12字节,因为f的数据类型是一个自定义类型d_t,且d_t的有效对齐为4字节;那么,g.f会占满0x7ffc425478f8
单元,也会占0x7ffc42547900
的4个字节;
由于g.x占1个字节,显然g.x的起始地址为0x7ffc42547904
;
由于g.g占31字节,因为成员g的数据类型也是一个自定义类型string,且string的有效对齐为1字节,那么g.g的起始地址为0x7ffc42547905
,直至占满后面的31个字节;(分布情况查看图例)
由于g.h占2字节,g.h的有效对齐为2字节,g.h的起始地址可以是0x7ffc42547924
;
由于g.i占4字节,g.i的有效对齐为4字节,显然地址0x7ffc42547926
不是4字节对齐,故g.i的起始地址不能是0x7ffc42547926
;必须重新开辟一个新的默认对齐单元
,所以g.i的起始地址为0x7ffc42547928
;那么0x7ffc42547920
的默认对齐单元
的最后两个字节为填充字节。
最后,对于结构体g_t整体,它的有效对齐为8字节,所以0x7ffc42547928
起始的8个字节都要算作是g_t所占用的内存,而不是0x7ffc42547928
起始的4个字节(虽然实际只使用了前面4字节)。
好了,前面提出的疑问"有效对齐为8字节,而g_t的原始大小为69字节,理论上g_t的实际大小为72字节应该是足够存储,但为什么实际大小是80字节呢?",你搞清楚了吗!
为什么对齐
到了这个阶段,可能有人会问,为什么要有对齐呢?
对齐的设计是为了提高代码的可移植性。因为不同架构平台(X86,ARM等),memory bus,cache,memory page都是8字节或8字节整数倍。有些平台仅支持对齐的数据访问,那么在这种平台上进行非对齐的数据访问操作,就会导致程序异常。当前,目前大多数平台都支持非对齐,那么代价就是 做同样一件事,非对齐比对齐消耗的资源更多,处理速度相对较慢。例如,想要将某个非对齐的数据写入当前地址,首先访问当前地址,发现不对齐,那么需要继续"找到"一个对齐的地址,然后再次访问这个对齐的新地址,然后写入数据。在这个过程中,显然发生了两次访问地址的动作。如果数据是对齐的,那么一次访问就可以搞定。其实,对齐的设计体现了以空间换时间的思想。
我们知道,默认对齐是8字节(64位系统),有些应用场景下,结构体中不能有填充字节(1字节对齐),否则数据的解析会发生未知的异常,这时就需要我们修改默认对齐字节数,那么,要怎么做呢?
对齐设置
将结构体定义为1字节对齐,有两种方法,都是将结构体成员的内存紧凑分配
方法一
#pragma pack(1)
typedef struct _apple_s
{
struct _fruit_s* p_fruit;
char* name;
}apple_t;
typedef struct _fruit_s
{
int type;
int weight;
float price;
char a;
}fruit_t;
#pragma pack(0)
方法二
typedef struct _apple_s
{
struct _fruit_s* p_fruit;
char* name;
}__attribute__((packed)) apple_t;
typedef struct _fruit_s
{
int type;
int weight;
float price;
char a;
}__attribute__((packed)) fruit_t;
QQ讨论群:679603305