结构体成员内存对齐问题

在讲内存对齐之前,先介绍一个相关的概念——偏移量

把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。

简单来说,在结构体中偏移量指的是结构体变量中成员的地址和结构体地址的差。

考虑下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义一个结构体foo,其中含有两个成员
struct Foo{
char flag;
int i;
};
int main(int argc,char* argv[])
{
Foo foo;
foo.flag='T';
// 试图通过由foo.flag成员的位置+1得到foo.i的地址
int *pi=(int*)(&foo.flag+1);
// 对上一步得到的地址中存储的成员进行赋值
*pi=0x01020304;
// 输出foo.flag和foo.i的值
printf("flag=%c,i=%x\n",foo.flag,foo.i);
return 0;
}

上面的代码中,定义了一个结构体,包括一个字符成员flag和整型成员i。在main函数中想通过指针方式将结构体整型成员赋值为0x01020304,但程序运行的结果实际值为0x01,赋值错误。
其类的成员布局在Clang中生成的IR代码为:

1
%struct.Foo = type { i8, i32 }

运行结果:

该程序的问题就在指针的赋值处,也就是int *pi=(int*)(&foo.flag+1);这里。错误在于误以为结构体字符成员flag的地址加1就是整型成员i的地址,然后给该地址赋值,期望i会得到应有的赋值。但赋值结果并非是所期望的那样。导致这个问题的根源是内存字节对齐

内存字节对齐是指,为了保证CPU对内存的访问效率,各种类型数据需要按照一定的规则在内存中存放,而不是完全按字节挨字节的顺序存放。每种数据类型的默认对其长度依赖于编译器的具体实现,不同的编译器可能有所不同。大多数情况下,基本数据类型的对其长度就是自己数据类型所占空间大小(sizeof值)

例如,char类型占用一个字节,那么对齐长度就是一个字节;int型占4个字节,那么对齐长度就是四个字节,double类型占8个字节,那么其对齐长度就是8个字节。

对于结构体数据成员,默认的字节对齐一般满足以下几个准则。

  1. 结构体变量的首地址能够被其最宽数据类型成员大小整除
  2. 结构体每个成员相对结构体首地址的偏移量都是该成员本身大小的整数倍,如有需要会在成员之间填充字节。(0被认为是任何数的整数倍)
  3. 结构体变量所占总空间的大小必定是最宽数据类型每个数据成员类型大小的整数倍。如有需要会再最后一个成员末尾填充若干字节,使得结构体所占空间大小是最宽数据类型大小的整数倍。
  4. union成员取最大的成员的字节数作为其大小
  5. 由于结构体类型需要考虑到字节对齐的情况,所以不同的成员声明顺序会影响结构体的大小。

在本文最开始的代码中,结构体foo中的整型成员i占用4个字节,是占用空间最多的成员,所以foo必须驻留在4的整数倍内存地址。字符成员flag的起始地址即为foo的起始地址,flag占用1个字节。整型数据成员i的起始地址因为必须是4的整数倍,所以不能直接存放于&flag+1的位置(flag已占用了一个字节,其偏移量为1,flag+1地址不再是4的整数倍),而是存放于&flag+4的位置。因此,flag后面的有3个字节浪费掉了,这样,foo一共需要占用8个字节的内存空间,而不是5个字节(chat类型和int类型的sizeof之和)

如图,foo的成员在内存中存放的布局为,每格为1字节。

上面的代码中,给&flag+1地址处赋值为一个4字节的整数0x01020304,因为有3个字节没有影响到变量i,所以赋值结果为1.

所以,想要给成员i正确赋值的代码为:

1
2
3
4
5
6
7
foo.flag='T';
// 不使用flag的地址加1给变量i赋值吗,直接使用foo.i的地址赋值。
int *pi=&foo.i;
// 对上一步得到的地址中存储的成员进行赋值
*pi=0x01020304;
// 输出foo.flag和foo.i的值
printf("flag=%c,i=%x\n",foo.flag,foo.i);

字节对齐的细节与具体编译器的实现有关,不同的平台可能会有所不同。一些编译器允许在代码中通过预处理器指令#pragma pack(n)或类型属性__attribute__((packed))来改变默认的内存对其条件。

再分析下以下一份代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct student{
int year;
double math;
//联合
union hold{
char GPA;
int degit;
char letter;
}hold;
};

int main(void){
sizeof(student);
return 0;
}
// 输出结果为24,编译平台为windows7x64,GCC5.2.0

student类在Clang中生成的IR代码为:

1
2
%struct.student = type { i32, double, %"union.student::hold" }
%"union.student::hold" = type { i32 }
成员 大小 偏移量
(int 4byte)year 4 0
(double 8byte)math 8 8
(union (int)4byte)hold 4 16
补4字节 24

year的偏移量为0,math的偏移量为sizeof(year)+4byte,即为8byte,而hold的偏移量为math的偏移量(8)加上sizeof(math)(8)的大小,即为16,,而union的大小是其占用空间最大的成员(即int degit;(4byte)),sizeof(student)的大小为最后一个成员的偏移量加上其大小(16+4=20),但是得到的结果并不是所有成员大小的整数倍,所以会在hold之后补上4个字节以满足该要求。所以,sizeof(student)的大小为24byte。

突然觉得一图胜千言…..

可以通过#pragma pack(ALIGN_NUM)预处理指令来指定对齐:

1
2
3
4
5
6
// sizeof(A) == 16
struct A{
int ival;
bool b;
double dval;
};

使用自定义对齐(1为不对齐):

1
2
3
4
5
6
7
8
// sizeof(A) == 13
#pragma pack(32)
struct A{
int ival;
bool b;
double dval;
};
#pragma pack()

C++11之后引入了alignof关键字,可以得到一个类型的对齐大小。

如下面结构:

1
2
3
4
5
6
7
8
9
10
11
12
struct A
{
char c;
int ival;
double dval;
short sival;
};
int main()
{
printf("%d\n", alignof(A));
}
// output: 8
全文完,若有不足之处请评论指正。

微信扫描二维码,关注我的公众号。

本文标题:结构体成员内存对齐问题
文章作者:查利鹏
发布时间:2016年06月02日 10时12分
本文字数:本文一共有1.8k字
原始链接:https://imzlp.com/posts/61962/
许可协议: CC BY-NC-SA 4.0
文章禁止全文转载,摘要转发请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!