看这样一个笔试题:
#include <iostream>
using namespace std;
#pragma pack(8)
struct example1
{
short a;
int b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
#pragma pack()
int main(int argc, char* argv[])
{
example2 struct2;
cout << sizeof(example1) << endl;
cout << sizeof(example2) << endl;
cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;
return 0;
}
问程序的输出结果是什么?
答案是 8,16,4
1. 自然对界
struct是一种复合数据类型,其构成元素既可以是基本数据类型的变量,也可以是复合数据类型(如struct、union等)。对于struct,编译器会自动进行成员变量的对齐,以提高运算效率。缺省情况下,编译器为struct的每个成员按其自然对界(natural alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界指按struct的成员中size最大的成员对齐,例如:
struct natural_align
{
char a;
short b;
char c;
};
在上述struct中,size最大的是short,其长度为2字节,因而struct中的char成员a、c都以2为单位对齐,sizeof(natural_align)的结果等于6。如果改为:
struct natural_align
{
char a;
int b;
char c;
};
其结果则变为12。
2. 指定对界
我们也可以屏蔽掉默认的对齐长度,编译器提供了”#pragma pack”来让用户自定义对齐长度。
语法:#pragma pack([push|pop][, identifier][, n])
调用pack时不指定参数,n将被设成默认值;
push:可选参数,将当前指定的packing alignment进行压栈操作(这里的栈是编译器的栈),同时设置当前的packing alignment为n;如果n没有指定,则只将当前的packing alignment压栈。
pop:可选参数,从栈中删除最顶端的记录;如果没有指定n,则当前栈顶记录即为新的packing alignment;如果指定了n,则n将成为新的packing aligment。
n:可选参数,指定packing alignment的数值,以字节为单位,缺省数值是8,合法的数值是1、2、4、8、16。
identifier:可选参数,当同push一起使用时,赋予当前被压入栈中的记录一个名称;当同pop一起使用时,pop出所有的记录直到identifier被pop出,如果identifier没有被找到,则忽略pop操作。
#pragma pack(n)
将按照n个字节对齐
#pragma pack()
恢复默认对齐长度
#pragma pack(push,n)
把原来对齐长度压栈,并设新的为n
#pragma pack(pop)
恢复栈顶的对齐长度
3. 完整的对齐规则
总结整理猜测 by liuyuan185442111
- 每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个来进行内存地址对齐;
- 复杂类型(如结构体)的对齐参数是它最长成员的对齐参数;
- 结构体s1做为结构体s2的成员变量时s1的成员变量的内存布局和它单独出现时的内存布局相同;
- 数组成员的内存分配情况完全等同于单个数组元素的堆砌,例如int a[3];就相当于int a1,a2,a3;一样;
- 结构体对齐后的长度必须是所有成员最大对齐参数和指定对齐参数中较小的一个的整数倍。
例一
struct s1
{
char a;
int b;
};
struct s2
{
char c;
s1 d;
long long e;
};
sizeof(s1)是8,s1的对齐参数是最大的成员也就是b的大小,为4。
c占1字节;d按4字节对齐;e占8字节,e按8字节对齐,所以内存布局就是这样:
c---a---
bbbb----
eeeeeeee
检验如下:
struct s2 t;
printf("%d",sizeof(struct s2));
printf(",%d",(unsigned int)&t.d-(unsigned int)&t.c);
printf(",%d",(unsigned int)&t.d.b-(unsigned int)&t.d);
printf(",%d\n",(unsigned int)&t.e-(unsigned int)&t.d.b);
//输出是24,4,4,8.
例二
在例一开头加上#pragma pack(4)
,输出结果为20,4,4,4
内存布局为:
c---
a---
bbbb
eeee
eeee
延伸至类
c++的类又有点不一样,因为类中可能有虚指针,指向虚基类的指针。以下测试和结论基于window和vc。
继承
class A
{
int ma;
};
class B:A
{
int mb;
};
B b;
b的内存结构是ma,mb依次排列。也就是说,普通继承关系,派生类对象的内存布局是先基类的成员,后派生类自己的成员。具体规则和struct中有struct成员的规则相同。
虚函数
带有虚函数的类,会有一个指向虚函数表的指针,这个指针会放在所有数据成员的前边,也就是说这个指针在对象内存区块的起始位置。
class A
{
int ma;
public:
A():ma(1) {}
virtual ~A() {}
};
class B:A
{
int mb;
public:
B():mb(2) {}
};
B b;
b的内存布局是:
00 31 42 00 // 指向虚函数表的指针
01 00 00 00
02 00 00 00
虚继承
class A
{
int a;
public:
A():a(1){}
};
class B:virtual A
{
int b;
public:
B():b(2){}
};
class C:virtual A
{
int c;
public:
C():c(3){}
};
class D:B,C
{
int d;
public:
D():d(4){}
};
void main()
{
A a;
B b;
C c;
D d;
}
结果:
a:
01 00 00 00
b:
1C 00 47 00 // 指针
02 00 00 00
01 00 00 00
c:
28 00 47 00 // 指针
03 00 00 00
01 00 00 00
d:
40 00 47 00 02 00 00 00
34 00 47 00 03 00 00 00
04 00 00 00 01 00 00 00
b和c起始地址有一个指针,指向一个关于偏移量的数组,用来实现虚继承,关于这个指针,有些复杂,就不说了。
还有一点,b,c,d中,class A中的数据都被放到了末尾。
例子
class A
{
double a;
};
class B:virtual public A
{
int b;
};
//sizeof(A):8,sizeof(B):16
class A
{
double a;
public:
virtual ~A(){}
};
class B:public A
{
int b;
};
//sizeof(A):16,sizeof(B):24
静态变量
static成员存储在静态区,不占用类的大小。
这样对类取sizeof和对象的sizeof就相同了。
比如:
class Test
{
static int i;
};
int Test::i = 1;
static int i;
相当于声明;int Test::i = 1;
才是定义。
i相当于一个全局变量,不过作用域限于Test。
参考
http://blog.chinaunix.net/uid-25445243-id-2354324.html
C++类的大小——sizeof()