探究结构体的对齐问题,使得我们更好的理解结构体在内存中的布局。
自定义字节对齐数值时,内存偏移计算
默认情况下编译器会对结构体进行字节对齐,提高访问速度。形如下面的例子允许自定义“字节对齐数值”(这个词不一定准确哈):
#pragma pack(4) //按4字节对齐
typedef struct _Product
{
int nId;
string strName;
double dPrice;
char chFlag;
}Product;
#pragma pack()
需要强调的是字节对齐跟结构体中各元素的顺序有关(下面会有例子说明)。
下面用一个表格来描述上述结构体的内存分布问题。sizeof(string)在VS2013下是28;在MinGW 5.3.0 32bit是24。
成员 | 长度 | min(自身长度,字节对齐数值) | 当前地址是否是min的倍数 | 实际起始地址 | 实际地址范围 | 当前地址 |
---|---|---|---|---|---|---|
nId | 4 | min(4,4)=4 | 无 | 0 | 0-3 | 4 |
strName | 28 | min(28,4)=4 | 1倍,是 | 4 | 4-31 | 32 |
dPrice | 8 | min(8,4)=4 | 8倍,是 | 32 | 32-39 | 40 |
chFlag | 1 | min(1,4)=1 | 40倍,是 | 40 | 40 | 41 |
Product | 无 | min(A,4)=4 | 否,41,42,43补 | 44 | 44 | 44 |
可得,sizeof(Product) = 44(最大元素的长度的倍数)。
采用系统默认对齐方式,内存偏移计算
好,下面咱们不自定义对齐数值,采用编译器默认:
typedef struct _Product
{
int nId;
string strName;
double dPrice;
char chFlag;
}Product;
下面用一个表格来描述上述结构体的内存分布问题。sizeof(string)在VS2013下是28;在MinGW 5.3.0 32bit是24。
成员 | 长度 | min(自身长度,字节对齐数值) | 当前地址是否是min的倍数 | 实际起始地址 | 实际地址范围 | 当前地址 |
---|---|---|---|---|---|---|
nId | 4 | min(4,4)=4 | 无 | 0 | 0-3 | 4 |
strName | 28 | min(28,4)=4 | 1倍,是 | 4 | 4-31 | 32 |
dPrice | 8 | min(8,8)=8 | 4倍,是 | 32 | 32-39 | 40 |
chFlag | 1 | min(1,1)=1 | 40倍,是 | 40 | 40 | 41 |
Product | 无 | min(A,8)=8 | 否,42-47补 | 48 | 48 | 48 |
可得,sizeof(Product) = 48(最大元素的长度的倍数)。
从上面的情况可知,字节对齐数值按每一个子项元素类型的自然边界对齐(natural alignment),也就是说自然边界对齐即为默认对齐方式。
结构体字节对齐计算方式,跟各元素的顺序有关
下面再举一例来强化一下。
typedef struct _Product
{
int nId;
double dPrice;
string strName;
char chFlag;
}Product;
下面用一个表格来描述上述结构体的内存分布问题。sizeof(string)在VS2013下是28;在MinGW 5.3.0 32bit是24。
成员 | 长度 | min(自身长度,字节对齐数值) | 当前地址是否是min的倍数 | 实际起始地址 | 实际地址范围 | 当前地址 |
---|---|---|---|---|---|---|
nId | 4 | min(4,4)=4 | 无 | 0 | 0-3 | 4 |
dPrice | 8 | min(8,8)=8 | 否,4-7补 | 8 | 8-15 | 16 |
strName | 28 | min(28,4)=4 | 4倍,是 | 16 | 16-43 | 44 |
chFlag | 1 | min(1,1)=1 | 44倍,是 | 44 | 44 | 45 |
Product | 无 | min(A,8)=8 | 否,45-47补 | 48 | 48 | 48 |
可得,sizeof(Product) = 48(最大元素的长度的倍数)。顺序不同,sizeof的值可能不同。
结构体如果不指定字节对齐数值,那么将小字节元素放在前面,将会更加节约内存。
关于结构体char元素的地址输出:
std::cout<<这个操作符对char*是有重载的
所以如下语句
std::cout<<Product.chFlag<<&(Product.chFlag)<<endl;
都会输出该项的值,而不会输出其地址,所以这里要换成printf.
关于对象或容器作为结构体的元素
测试得知,无论Product::strName赋值多长,该字符串有多少个字符,其在结构体内部的地址偏移计算,始终是按照28个字节(MinGW32)来计算的。也就是是strName的字符串内容实际上并不在结构体的内存空间里,所以序列化要格外注意。
如下宏定义可以获取已知结构体的相关值
//各个元素的sizeof值
#define StructFieldSize(type, field) sizeof(((type*)0)->field)
//各个元素的偏移量
#define StructOffset(type, member) ((size_t)&((type*)0)->member)
//根据已经分配内存的的结构体对象,的某一个元素的地址获取结构体首地址
#define GetStructHeaderPoint(ptr, type, member)((type *)((char*)(ptr)-(unsigned long)(&((type *)0)->member)))