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

引言

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

实际环境

  • 系统环境 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个规则:ui

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

下面的demo会为你们解释以上规则:spa

代码

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个字节对齐,其取值为一、二、四、八、16,默认是8,使用伪指令#pragma pack(),取消自定义字节对齐方式。若是设置#pragma pack(1),就是让结构体没有填充字节,实现空间“无缝存储”,这对跨平台传输数据来讲是友好和兼容的。结构体中含有#pragma pack预处理宏,除了要遵循上面3个准则,还要遵循如下2个规则:.net

  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个准则**的前提下,有本身的相应规则也要遵照。结构体成员在排列时数据类型要遵循从小到大排列,这样能尽量的节省空间。指针

参考连接

blog.sina.cn/dpool/blog/… c.biancheng.net/cpp/html/46… hubingforever.blog.163.com/blog/static…code

相关文章
相关标签/搜索