struct内存对齐1:gcc与VC的差别

struct内存对齐:gcc与VC的差别

内存对齐是编译器为了便于CPU快速访问而采用的一项技术,对于不同的编译器有不同的处理方法。

Win32平台下的微软VC编译器在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。Linux下的GCC奉行的是另外一套规则:任何2字节大小的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如long,double)都以4为对齐模数。这里说明一下,如果全部是char 那对齐就是1个字节。如果char  和short char放一块那就是 2个字节对齐。当然具体要看下面

char可以是任意地址处,short必须是2的倍数地址处,int必须是4倍数地址处

下面的程序可以验证:


#include <stdio.h>
#define OFFSET(TYPE,MEMBER)  ((int)(&(((TYPE*)0)->MEMBER)))
typedef struct
{
    int a;
    float b;
    char c;
    double d;
    int *pa;
    char *pc;
}Sta;
int main()
{
    printf("a_=%d\n",OFFSET(Sta,a));
    printf("b_=%d\n",OFFSET(Sta,b));
    printf("c_=%d\n",OFFSET(Sta,c));
    printf("d_=%d\n",OFFSET(Sta,d));
    printf("pa_=%d\n",OFFSET(Sta,pa));
    printf("pc_=%d\n",OFFSET(Sta,pc));
    return 0;
}

在VC上的结果是:

struct内存对齐1:gcc与VC的差别

而在linux下的结果却是:

struct内存对齐1:gcc与VC的差别

主要是对于double类型,VC采用的是8对齐,而gcc采用的是4对齐


(1)什么是字节对齐

一个变量占用 n 个字节,则该变量的起始地址必须能够被 n 整除,即: 存放起始地 址 % n = 0,对于结构体而言, 这个 n 取其成员中的数据类型占空间的值最大的那个。

(2)为什么要字节对齐

内存空间是按照字节来划分的,从理论上说对内存空间的访问可以从任何地址开始,但是在实际上不同架构的 CPU 为了提高访问内存的速度,就规定了对于某些类型的数据只能从特定的起始位置开始访问。这样就决定了各种数据类型只能按照相应的规则在内存空间中存放,而不能一个接一个的顺序排列。

举个例子,比如有些平台访问内存地址都从偶数地址开始,对于一个 int 型(假设 32 位系统),如果从偶数地址开始的地方存放,这样一个读周期就可以读出这个 int 数据,但是如果从奇数地址开始的地址存放,就需要两个读周期,并对两次读出的结果的高低字节进行拼凑才能得到这个 int 数据,这样明显降低了读取的效率。

(3)如何进行字节对齐

每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(不指定则取默认值)中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。

这个规则有点苦涩,可以把这个规则分解一下,前半句的意思先获得对齐值后与指定对齐值进行比较,其中对齐值获得方式如下:

1. 数据类型的自身对齐值为:对于 char 型数据,其自身对齐值为 1,对于 short 型 为 2,对于 int, long,float 类型,其自身对齐值为 4,对于 double 类型其自身对齐值为 8,单位为字节。

2.结构体自身对齐值:其成员中自身对齐值最大的那个值。

其中指定对齐值获得方式如下:

#pragma pack (value)时的指定对齐值 value。

未指定则取默认值。

后半句的意思是主要是针对于结构体的长度而言,因为针对数据类型的成员,它仅有一个对齐参数,其本身的长度、于这个对齐参数,即 1 倍。对于结构体而言,它可能使用 了多种数据类型,那么这句话翻译成对齐规则:每个成员的起始地址 % 自身对齐值 = 0,如果不等于 0 则先补空字节直至这个表达式成立。

换句话说,对于结构体而言,结构体在内存的存放顺序用如下规则即可映射出来:

(一)每个成员的起始地址 % 每个成员的自身对齐值(注意这里不是结构体的自身对齐值) = 0, 如果不等于 0 则先补空 字节直至这个表达式成立;

(二)结构体的长度必须为结构体的自身对齐值的整数倍,不够就补空字节。

不同平台下对齐系数:

每个特定平台的编译器都有一个默认的对齐系数,gcc中是4,VC中是8。也可以通过于编译命令#pragmapack(n)来指定该系数,经测试gcc中n的值只能是1,2和4。

举个例子:

#pragmapack(8)

structA{

char a;

long b;

};

structB{

char a;

structA b;

long c;

};

structC{

char a;

structA b;

double c;

};

structD{

char a;

structA b;

double c;

int d;

};

structE{

char a;

int b;

structA c;

double d;

};

在VC中:

对于 struct A 来说,

对于 char 型数据,其自身对齐值为 1,对于 long 类型,其自 身对齐值为 4, 结构体的自身对齐值取其成员最大的对齐值,即大小 4。那么 struct A 在内存中的顺序步骤为:

(1) char a, 地址范围为 0x0000~0x0000,起始地址为 0x0000,满足 0x0000 % 1 = 0,这个成员字节对齐了。

(2) long b, 地址起始位置不能从 0x00001 开始, 因为 0x0001 % 4 != 0, 所 以先补空字节,直到 0x00003 结束,即补 3 个字节的空字节,从0x00004 开始存放 b, 其地址范围为 0x00004~0x0007.

(3)此时成员都存放结束,结构体长度为 8,为结构体自身对齐值的 2 倍,符合条件 (二).

此时满足条件(一)和条件(二),struct A 中各成员在内存中的位置为:a*** b ,si zeof(structA) = 8。 (每个星号代表一位, 成员各自代表自己所占的位,比如 a 占一位, b 占四位)

对于 struct B,

里面有个类型为 structA 的成员 b 自身对齐值为 4,对于 long 类 型,其自身对齐值为 4. 故 struct B 的自身对齐值为 4。那么 structB 在内存中的顺序步骤为:

(1) char a, 地址范围为 0x0000~0x0000,起始地址为 0x0000,满足 0x0000 % 1 = 0,这个成员字节对齐了。

(2) struct A b, 地址起始位置不能从 0x00001 开始, 因为 0x0001 % 4 != 0, 所以先补空字节,直到 0x00003 结束,即补 3 个字节的空字节,从0x00004 开始存 放 b,其地址范围为0x00004~0x00011.

(3) long c,地址起始位置从 0x000012 开始, 因为 0x0012 % 4 = 0,其地 址范围为 0x00012~0x0015.

(4)此时成员都存放结束,结构体长度为 16,为结构体自身对齐值的 4 倍,符合条件 (二).

此时满足条件(一)和条件(二),struct B 中各成员在内存中的位置为:a*** b c ,sizeof(struct B) = 16。(每个星号代表一位,成员各自代表自己所占的位,比如 a 占 一位,b 占八位,c 占四位)

对于 struct C,

里面有个类型为 structA 的成员 b 自身对齐值为 4,对于 double 类型,其自身对齐值为 8. 故 struct C 的自身对齐值为 8。那么 struct C 在内存中的顺 序步骤为:

(1) char a, 地址范围为 0x0000~0x0000,起始地址为 0x0000,满足 0x0000 % 1 = 0,这个成员字节对齐了。

(2) struct A b, 地址起始位置不能从 0x00001 开始, 因为 0x0001 % 4 != 0, 所以先补空字节,直到 0x00003 结束,即补 3 个字节的空字节,从 0x00004开始存放 b,其地址范围为0x00004~0x00011.

(3) double c,地址起始位置不能从 0x000012 开始, 因为 0x0012 % 8 != 0,所以先补空字节,直到 0x000015 结束,即补 4 个字节的空字节,从 0x00016开始存放 c,其地址范围为0x00016~0x0023.

(4)此时成员都存放结束,结构体长度为 24,为结构体自身对齐值的 3 倍,符合条件 (二).

此时满足条件(一)和条件(二),struct C 中各成员在内存中的位置为:a*** b ** ** c,sizeof(struct C) = 24。(每个星号代表一位,成员各自代表自己所占的位,比 如 a 占一位,b 占八位,c 占八位)

对于 struct D,

自身对齐值为 8。前面三个成员与 struct C 是一致的。对于第四 成员 d,因为 0x0024 % 4= 0, 所以可以从 0x0024 开始存放 d, 其地址范围为 0x 00024~0x00027.此时成员都存放结束,结构体长度为 28,28 不是结构体自身对齐值8 的倍数,所以要在后面补四个空格,即在 0x0028~0x0031 上补四个空格。补完了, 结构体长度为 32, 为结构体自身对齐值的 4 倍,符合条件(二).

此时满足条件(一)和条件(二),struct D 中各成员在内存中的位置为:a*** b ** ** c d ****,sizeof(struct D) = 32。(每个星号代表一位,成员各自代表自己所占 的位,比如 a 占一位,b 占八位,c 占八位, d 占四位)。

对于 struct E,

各成员在内存中的位置为:a***b c d, sizeof(struct E) = 2 4。(每个星号代表一位,成员各自代表自己所占的位,比如 a 占一位,b 占四位,c 占八位, d 占八位)。

通过 struct D 和 struct E 可以看出,在成员数量和类型一致的情况,后者的所占 空间少于前者,因为后者的填充空字节要少。如果我们在编程时考虑节约空间的话,应该遵循将变量按照类型大小从小到大声明的原则, 这样尽量减少填补空间。另外,可以在填充空字节的地方来插入reserved 成员, 例如

struct A { char a; char reserved[3]; int b;};

这样做的目的主要是为了对程序员起一个提示作用,如果不加则编译器会自动补齐。

在gcc中

由于对齐系数最大只能为4,所以上述结构体占内存大小为:8,16,20,24,24。

验证:

1、默认情况(n=4)

struct st1 {
char ch;//长度1<n,按1对齐,0%1=0,起始相对位置=0;存放区间[0]
int num;//长度4=n,按4对齐, 4%4=0,起始相对位置=4;存放区间[4,7]
long lv;//长度4=n,按4对齐,8%4=0,起始相对位置=8;存放区间[8,11]
};

整个结构体成员对齐后所占的区间为[0,8],占12个字节,接着结构体本身对齐,成员中最长的是4,n也等于4,所以结构体本身按4对齐(即对齐系数)。
整个结构体的大小 = 比整个结构体数据成员所占的总空间大或相等且和对齐系数求模结果为0、与之距离最近的数。
本例中,12%4=0,所以结构体st1占12个字节的空间。

2、#pragmapack(1)(即n=1)

struct st1 {
char ch;//长度1=n,按1对齐,0%1=0,起始相对位置=0;存放区间[0]
int num;//长度4>n,按n对齐, 1%1=0,起始相对位置=0;存放区间[1,4]
long lv;//长度4>n,按n对齐,5%1=0,起始相对位置=5;存放区间[5,8]
};

整个结构体成员对齐后所占的区间为[0,8],占9个字节,接着结构体本身对齐,成员中最长的是4,n等于1,所以结构体本身按1对齐(即对齐系数)。
整个结构体的大小 = 比整个结构体数据成员所占的总空间大或相等且和对齐系数求模结果为0、与之距离最近的数。
本例中,9%1=0,所以结构体st1占9个字节的空间。

3、#pragmapack(2)(即n=2)

struct st1 {
char ch;//长度1<n,按1对齐,0%1=0,起始相对位置=0;存放区间[0]
int num;//长度4>n,按n对齐, 2%2=0,起始相对位置=2;存放区间[2,5]
long lv;//长度4>n,按n对齐,6%2=0,起始相对位置=6;存放区间[5,8]
};

整个结构体成员对齐后所占的区间为[0,8],占9个字节,接着结构体本身对齐,成员中最长的是4,n等于2,所以结构体本身按2对齐(即对齐系数)。
整个结构体的大小 = 比整个结构体数据成员所占的总空间大或相等且和对齐系数求模结果为0、与之距离最近的数。
本例中,10%2=0,所以结构体st1占10个字节的空间。

为什么说#pragmapack(n)中n只能是1,2,4呢?
比如3,如果n=3,在编译的时候会警告“对齐边界必须是 2 的较小次方,而不是 3”,也就是说是不起作用的,按默认对齐系数对齐。
再如8,会有什么结果?看下一例:

4、#pragmapack(8)(即n=8)

struct siz {
char v1;
long long v2;
short v3;
int v4;
};

如果8起作用,分析一下:
struct siz {
char v1;//长度1<n,按1对齐,0%1=0,起始相对位置=0;存放区间[0]
long long v2;//长度8=n,按8对齐,8%8=0,起始相对位置=8;存放区间[8,15]
short v3;//长度2<n,按2对齐,16%2=0,起始相对位置=16;存放区间[16,17]
int v4;//长度4<n,按4对齐,20%4=0,起始相对位置=20;存放区间[20,23]
};

整个结构体成员对齐后所占的区间为[0,23],占24个字节,接着结构体本身对齐,成员中最长的是8,n等于8,所以结构体本身按8对齐(即对齐系数)。24%8=0,所以占24个字节。
然而,很不幸,运行的结果是20.
接下来,用默认的对齐系数4来分析一下:
struct siz {
char v1;//长度1<4,按1对齐,0%1=0,起始相对位置=0;存放区间[0]
long long v2;//长度8>4,按4对齐,4%4=0,起始相对位置=4;存放区间[4,11]
short v3;//长度2<4,按2对齐,12%2=0,起始相对位置=12;存放区间[12,13]
int v4;//长度4=4,按4对齐,16%4=0,起始相对位置=16;存放区间[16,19]
};
整个结构体成员对齐后所占的区间为[0,19],占20个字节,接着结构体本身对齐,成员中最长的是8,n等于4,所以结构体本身按4对齐(即对齐系数)。20%4=0,所以占20个字节。与运行结果一致。

综上分析,当n=8的时候gcc仍然使用的是默认的对齐系数4.

位域

有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:

struct 位域结构名

{ 位域列表 };

其中位域列表的形式为: 类型说明符 位域名:位域长度

例如:

struct bs
{
int a:8;
int b:2;
int c:6;
};

如果结构体中含有位域(bit-field),那么VC中准则又要有所更改:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;

struct s1
  {
    int i:8;
    char j:4;
    int a:20;
    double b;
  };

struct s2
  {
    int i:8;
    char j:4;
    int a:21;
    double b;
  };

sizeof(struct s1), VC中为24,gcc中为 12

sizeof(struct s2), VC中为24,gcc中为 16

4) 如果位域字段之间穿插着非位域字段,则不进行压缩; 
备注:
结构体
typedef struct
{
char c:2;
double i;
int c2:4;
}N3;
在GCC下占据的空间为16字节,在VC下占据的空间是24个字节。

5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。

ps:

  • 对齐模数的选择只能是根据基本数据类型,所以对于结构体中嵌套结构体,只能考虑其拆分的基本数据类型。而对于对齐准则中的第2条,确是要将整个结构体看成是一个成员,成员大小按照该结构体根据对齐准则判断所得的大小。
  • 类对象在内存中存放的方式和结构体类似,这里就不再说明。需要指出的是,类对象的大小只是包括类中非静态成员变量所占的空间,如果有虚函数,那么再另外增加一个指针所占的空间即可。
上一篇:【MyBatis源码分析】插件实现原理


下一篇:hdu2647解题报告