结构体的内存对齐

结构体是一种结构型的数据,可以把多种不同的基本数据类型有结构的组织在一起,定义为一个新的数据类型。那么这种类型的数据在内存中是如何存放的,有什么规则要遵循,还是按照数据类型所占内存大小顺序的排列下去就好了,下面就让我们来一窥究竟。
翠花,上代码!

struct CHStruct1 {
    double a;   
    char b;     
    int c;      
    short d;    
};
typedef struct CHStruct1 CHStruct1;

可以看到CHStruct1这样一个标准的结构体定义,其中包括了一些基本的数据类型,那么在内存中这个结构体是如何存放的,占多大的空间,这些问题都需要我们去研究一下。为了计算内存大小,我们先看下每种数据类型在内存中所占大小,如下表:


数据类型在内存所占大小.png

首先我们假设这个结构体所占内存大小是按照每种数据类型的大小加起来的大小,那么我们打印这个结构体的大小应该是8+1+4+2=15,如果是这样我们就打印下看看:

NSLog(@"%lu",sizeof(struct1));

运行之后,实际打印的结果是:24。为什么结果和预想的不同,下面就要介绍重点了:结构体的内存对齐。

  1. 结构struct或联合union的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值(或默认值)和这个数据成员类型长度中,比较小的那个进行。在上一个对齐后的地方开始寻找能被当前对齐数值整除的地址.
  2. 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐。主要体现在,最后一个元素对齐后,后面是否填补空字节,如果填补,填补多少?对齐将按照#pragma pack指定的数值(或默认值)和结构(或联合)最大数据成员类型长度中,比较小的那个进行。
  3. 结合1、2推断:当#pragma pack(n)n值等于或超过所有数据成员类型长度的时候,这个n值的大小将不产生任何效果。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

看起来是不是很晕,有一种不可描述的感觉,下面就让我们一步步来拆解,还用上面的结构体来举例:

//内存对齐
//默认的对齐数是8
//linux环境的默认对齐数4
struct CHStruct1 {
    double a; //double类型占8字节,对齐数是8,由于是第一个元素,所以存放的位置是0-7字节
    char b; //1字节,接着上个元素位置,这里对齐数是1,8又是1的整数倍,所以b成员就存放在a成员的后面
    int c; //4字节,对齐数是4,上面的元素排到9,4不能被9整除,所以要补齐到12的位置,从下标为12的字节位置开始存放,也就是12,13,14,15这4个字节来存放成员c
    short d; //2字节,上面的成员排列到了15,这里要从下标16的字节开始存放,16能够整除2的对齐数,所以16,17这两个字节存放的是成员d   
};
typedef struct CHStruct1 CHStruct1;

从上面的代码的注释中,我们看到这个结构体在各自成员对齐排列后,最终排列到了下标为17的字节位置,也就是共用了18个字节来存放整个结构体,但刚刚sizeof(struct1)打印的结果并不是18,这就要引出最后一步整体对齐了,整体对齐意味着整个结构体占用的内存长度是自身成员对齐数最大的那个的整数倍,我们看到所有成员中,最大的对齐数是成员a,也就是8,那么最终对齐内存后,所占用内存长度需要是8的整数倍,刚才计算所有子成员对齐后使用了18个字节,要想被8整除,距18最近的数就是24,那么就要在第18个字节后面在补充6个字节,共24个字节,这下就该明白了NSLog中打印出来的24是怎么算出来的了。

那么字节对齐有什么意义呢,为什用更少的内存能解决的问题偏偏要浪费空间呢?这个问题就和CPU每次读取数据的方式有关了:

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总的来说,结构体的内存对齐是拿空间来换取时间的做法;

下面让我们更进一步,看看结构体嵌套的情况:

struct CHStruct2 {
    double a;   //8 (0-7)
    int b;      //4 (8 9 10 11)
    char c;     //1 (12)
    short d;    //2 13 (14 15) - 16
};
// 15 -> 16
typedef struct CHStruct2 CHStruct2;
struct CHStruct3 {
    double a;   //8 (0-7)
    int b;      //4 (8 9 10 11)
    char c;     //1 (12)
    short d;    //2 13 (14 15) - 16
    CHStruct2 struct2; //16 (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31)
};
//32
typedef struct CHStruct3 CHStruct3;
CHStruct2 struct2;
CHStruct3 struct3;
NSLog(@"%lu-%lu",sizeof(struct2),sizeof(struct3)); //打印16-32

打印的结果不出意料,是:16-32,和注释中的推算完全符合。struct2占16字节,struct3占32字节,但是这个计算结果真的是注释中这样的计算方式吗,我们认为struct3中的成员struct2是那个最大对齐数的元素,最终占用内存也是它的整数倍,这一切看起来似乎很正确,但是我们调整一下结构再看看:

//减少一些成员,再计算一次
struct CHStruct4 {
    int b;      //4 (0 1 2 3)
    char c;     //1 (4)
    CHStruct2 struct2; //16 5 (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31)
};
CHStruct4 struct4;
NSLog(@"%lu",sizeof(struct4));//打印24

如果按照上面代码注释中的思路,struct4无疑也应该占用32字节,打印结果却是24,是不是很费解,最大对齐元素应该是成员struct2,占用16字节,最终对齐结果应该是16的整数倍,为什么打印出来是24,我们不妨把结构体CHStruct4中的struct2按照原定义展开来看:

struct CHStruct4 {
    int b;      //4 (0 1 2 3)
    char c;     //1 (4)
//    CHStruct2 struct2; //16 5 (16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31)
    double a;   //8 5 (8 9 10 11 12 13 14 15)
    int f;      //4 (16 17 18 19)
    char g;     //1 (20)
    short d;    //2 21 (22 23) - 24
};
//24
typedef struct CHStruct4 CHStruct4;

这样看是不是一目了然,同样的计算方式,注释推算的最终结果变成24,而不是32了,所以我们得出了结论:在结构体嵌套情况下,计算结构体最大成员,并不是把嵌套其中的结构体当做一个整体成员去计算对齐,依然可以把它的成员展开成普通类型,看里面的最大对齐数成员的对齐数是多少,并从这个对齐数的整数倍位置开始对齐嵌套的结构体成员,按照对齐数整数倍方式逐个对齐,最终在计算最大对齐数(包含嵌套结构总的成员对齐数)的整数倍。
我们可以修改一下CHStruct4的定义,结论依然是成立的:

//我们调换一下CHStruct2中的成员位置
struct LGStruct2 {
    int b;      //4 (0 1 2 3)
    double a;   //8 (8 9 10 11 12 13 14 15)
    char c;     //1 (16)
    short d;    //2 13 (18 19) - 24
};
typedef struct CHStruct2 CHStruct2;

struct CHStruct5 {
    char c;     //1 (0)
    short d;    //2 1 (2 3)
    LGStruct2 struct2;
//    int f;      //4 (8 9 10 11)
//    double a;   //8 12 (16 17 18 19 20 21 22 23)
//    char g;     //1 (24)
//    short h;    //2 25 (26 27) - 32
};
typedef struct LGStruct5 LGStruct5;

struct CHStruct6 {
    char c;     //1 (0)
    LGStruct2 struct2;
//    int f;      //4 (8 9 10 11)
//    double a;   //8 12 (16 17 18 19 20 21 22 23)
//    char g;     //1 (24)
//    short h;    //2 25 (26 27) - 32
};
typedef struct CHStruct6 CHStruct6;
CHStruct5 struct5;
CHStruct6 struct6;
NSLog(@"%lu-%lu",,sizeof(struct5),sizeof(struct6)) //输出32-32

关于#pragma pack对齐数的概念:
1.#pragma pack(n)
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。这里规定的是上界,只影响对齐单元大于n的成员,对于对齐字节不大于n的成员没有影响。

可以认为处理器一次性可以从内存中读/写n个字节。对于大小小于n的成员,按照自己的对齐条件对齐,因为不论怎么放都可以一次性取出。对于对齐条件大于n个字节的成员,成员按照自身的对齐条件对齐和按照n字节对齐需要相同的读取次数,但按照n字节对齐节省空间。
通过预编译命令#pragma pack()取消自定义字节对齐方式。

也可以写成:
#pragma pack(push,n)
#pragma pack(pop)

2.__attribute__((aligned (n)))

__attribute__((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
__attribute__((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

需要注意的是:内存对齐的 对齐数 取决于 对齐系数 和 成员的字节数 两者之中的较小值。
举例说明:

struct test {
    char x1;
    short x2;
    float x3;
    char x4;
}

默认情况下,结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然边界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对齐,是该结构所有成员中要求的最大边界单元,因而test结构的自然对齐条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

当使用:#pragma pack(1)//让编译器对这个结构作1字节对齐

#pragma pack(1) //让编译器对这个结构作1字节对齐
struct test {
    char x1;
    short x2;
    float x3;
    char x4;
};
#pragma pack() //取消1字节对齐,恢复为默认4字节对齐

这时候sizeof(struct test)的值为8。

同理:使用__attribute__((packed))

#define PACKED __attribute__((packed))
struct PACKED test {
    char x1;
    short x2;
    float x3;
    char x4;
}test;

这时候sizeof( test)的值仍为8。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339