C语言结构体内存布局问题

引言

C语言结构体内存布局是一个老生常谈的问题,网上也看了一些资料,有些说的比较模糊,有些是错误的。本人借鉴了前人的文章,经过实践,总结了一些规则,如有错误,希望指正,不胜感激。

实际环境

  • 系统环境 macOS Sierra(10.12.4)
  • IDE Xcode(8.3)

概述

影响结构体内存布局有位域#pragma pack预处理宏两个情况,下面分情况说明。

正常情况

结构体字节对齐的细节和具体的编译器实现相关,但一般来说遵循3个准则:

  1. 结构体变量的首地址能够被其最宽基本类型成员的大小(sizeof)所整除。
  2. 结构体每个成员相对结构体首地址的偏移量offset都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。
  3. 结构体的总大小sizeof为结构体最宽基本成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

下面的demo会为大家解释以上规则:

代码

struct student {
  char name[5];
  double weight;
  int age;
};
struct school {
  short age;
  char name[7];
  struct student lilei;
};
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // insert code here...
    struct student lilei = {"lilei",112.33,20};
    printf("size of struct student: %lu\n",sizeof(lilei));
    printf("address of student name: %u\n",lilei.name);
    printf("address of student weight: %u\n",&lilei.weight);
    printf("address of student age: %u\n",&lilei.age);
    
    struct school shengli = {70,"shengli",lilei};
    printf("size of struct school: %lu\n",sizeof(shengli));
    printf("address of school age: %u\n",&shengli.age);
    printf("address of school name: %u\n",shengli.name);
    printf("address of school student: %u\n",&shengli.lilei);
  }
  return 0;
}

输出结果

解释规则

  1. 编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,做为结构体的首地址。(在本demo中struct school 包含 struct student,所以最宽的基本数据类型为doublesizeof(double)81606416152/8 = 2008020191606416112/8 = 200802014)。
  2. 为结构体的每一个成员开辟空间之前,编译器首先检查预开辟空间首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节(这也是为什么struct student weight成员的首地址是1606416160而不是1606416157但有很重要的一点要注意,这里的成员为基本数据类型,不包括char类型数组和结构体成员,char类型数组按1字节对齐,结构体成员存储的起始位置要从自身内部最大成员大小的整数倍地址开始存储,比如struct a里有struct b成员,b里有char,int,double等成员,那b存储的起始位置应该从8的整数倍开始。通过struct school成员内存分布可以看出来,school.name的首地址是1606416114,而不是1606416119school.student的首地址是1606416128,能被8整除,不能被24整除)。
  3. 结构体的总大小包括填充字节,最后一个成员出了满足上面两条之外,还必须满足第三条,否则必须在最后填充一定字节以满足要求(这也是为什么struct student占用字节数为24而不是20的原因)。

内存分布

student
school

扩展

细心的朋友可能发现&shengli.lilei(等效于shengli.lilei.name)的数值并不等于lilei.name,也就是说struct school shengli里的成员struct student lileistruct student lilei并不是指向同一块内存空间,是值拷贝开辟的一块新的内存空间,也就是说struct是值类型而不是引用类型数据结构。还有通过内存地址可以发现两个结构体变量的内存空间是在内存栈上连续分配的。

位域

结构体使用位域的主要目的是压缩存储,位域成员不能单独被取sizeof值。C99规定int,unsigned int,bool可以作为位域类型,但编译器几乎都对此做了扩展,允许其它类型存在。结构体中含有位域字段,除了要遵循上面3个准则,还要遵循以下4个规则:

  1. 如果相邻位域字端的类型相同,且位宽之和小于类型的sizeof大小,则后一个字段将紧邻前一个字段存储,直到不能容纳为止。
  2. 如果相邻位域字段的类型相同,但位宽之和大于类型的sizeof大小,则后一个字段将从新的存储单元开始,其偏移量为其类型大小的整数倍。
  3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式。
  4. 如果位域字段之间穿插着非位域字段,则不进行压缩。

下面的demo会为大家解释以上规则:

代码

typedef struct A {
  char f1:3;
  char f2:4;
  char f3:5;
  char f4:4;
}a;
typedef struct B {
  char  f1:3;
  short f2:13;
}b;
typedef struct C {
  char f1:3;
  char f2;
  char f3:5;
}c;
typedef struct D {
  char f1:3;
  char :0;
  char :4;
  char f3:5;
}d;
typedef struct E {
  int f1:3;
}e;
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // insert code here... 
    printf("size of struct A: %lu\n",sizeof(a));
    printf("size of struct B: %lu\n",sizeof(b));
    printf("size of struct C: %lu\n",sizeof(c));
    printf("size of struct D: %lu\n",sizeof(d));
    printf("size of struct E: %lu\n",sizeof(e));
  }
  return 0;
}

输出结果

解释规则

  1. struct A中所有位域成员类型都为char,第一个字节只能容纳f1f2f3从下一个字节开始存储,第二个字节不能容纳f4,所以f4也要从下一个字节开始存储,因此sizeof(a)结果为3
  2. struct B中位域成员类型不同,进行了压缩,因此sizeof(b)结果为2(不压缩方式没有进行验证,很抱歉)。
  3. struct C中位域成员之间有非位域类型成员,不进行压缩,因此sizeof(c)结果为3。
  4. struct D中有无名位域成员,char f1:33bitchar :0移到下1个字节(移动单位和具体位域类型有关,short移到下2个字节,int移到下4个字节),char :44bit,然后不能容纳char f3:5,所以要存到下1个字节,因此sizeof(d)结果为3
  5. 可能有人会疑惑,为什么sizeof(e)结果为4,不应该是只占用1个字节么?不要忘了上面提到的准则3

注意事项

  1. 位域的地址不能访问,因此不允许将&运算符用于位域。不能使用指向位域的指针也不能使用位域的数组(数组是种特殊指针)。
  2. 位域不能作为函数的返回结果。
  3. 位域以定义的类型为单位,且位域的长度不能超过所定义类型的长度。例如定义int a:33是不被允许的。
  4. 位域可以不指定位域名,但不能访问无名的位域。无名的位域只用做填充或调整位置,占位大小取决于该类型。例如char:0表示整个位域向后推一个字节,即该无名位域后的下一个位域从下一个字节开始存放,同理short:0int:0分别代表整个位域向后推两个和四个字节。当空位域的长度为具体数值N时(例如 int:2),该变量仅用来占N位。

pragma pack预处理宏

编译器的#pragma pack指令也是用来调整结构体对齐方式的,不同编译器名称和用法略有不同。使用伪指令#pragma pack(n),编译器将按照n个字节对齐,其取值为1、2、4、8、16,默认是8,使用伪指令#pragma pack(),取消自定义字节对齐方式。如果设置#pragma pack(1),就是让结构体没有填充字节,实现空间“无缝存储”,这对跨平台传输数据来说是友好和兼容的。结构体中含有#pragma pack预处理宏,除了要遵循上面3个准则,还要遵循以下2个规则:

  1. 对于结构体成员存放的起始地址的偏移量,如果n大于等于该成员类型所占用的字节数,那么偏移量必须满足默认的对齐方式,如果n小于该成员类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
    offsetof(item) = min(n, sizeof(item))
  2. 对于结构体的总大小,如果n大于所有成员类型所占用的字节数,那么结构的总大小必须为占用空间最大成员占用空间数的倍数,否则必须为n的倍数。

用法

#pragma pack(push)  //packing stack入栈,设置当前对齐方式
#pragma pack(pop)   //packing stack出栈,取消当前对齐方式
#pragma pack(n)     //n=1,2,4,8,16保存当前对齐方式,设置按n字节对齐
#pragma pack()      //等效于pack(pop)
#pragma pack(push,n)//等效于pack(push) + pack(n)

代码

#pragma pack(4)

typedef struct F {
  int f1;
  double f2;
  char f3;
}f;

#pragma pack()
#pragma pack(16)

typedef struct G {
  int f1;
  double f2;
  char f3;
}g;
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // insert code here...
    printf("size of struct D: %lu\n",sizeof(f));
    printf("size of struct E: %lu\n",sizeof(g));
  }
  return 0;
}

输出结果

解释规则

  1. struct F设置的对齐方式为4min(4, sizeof(int)) = 4,f14个字节,偏移量为0min(4, sizeof(double)) = 4f24个字节,偏移量为4min(4, sizeof(char)) = 1f31个字节,偏移量为12,最后整个结构体满足准则3sizeof(f) = 16
  2. struct G设置的对齐方式为16,比结构体中所有成员类型都要大,相当于没有生效,因此sizeof(f) = 24

总结

位域#pragma pack预处理宏的结构体在遵循3个准则的前提下,有自己的相应规则也要遵守。结构体成员在排列时数据类型要遵循从小到大排列,这样能尽可能的节省空间。

参考链接

http://blog.sina.cn/dpool/blog/s/blog_671d96d00100hhv9.html?vt
http://c.biancheng.net/cpp/html/469.html
http://hubingforever.blog.163.com/blog/static/17104057920122256134681/

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

推荐阅读更多精彩内容