C++对象的内存布局
内存布局是属于较深层次的知识,很多问题往深了讲都是不清楚内存布局的原理。最近读到一本书,里面讲了一部分C++对象的内存布局,让我对很多以前的问题都豁然开朗了。书上篇幅较大,我加上自己的理解总结了下。
分为三部分:简单对象,单继承,多继承
一:简单对象
- 非静态成员变量和虚函数是决定类大小的唯一两个因素
- 非静态成员变量在类的内存里会有字节对齐
- 如果对象中包含虚函数,会增加4个字节的空间,不论有多少个虚函数。
- 静态成员变量,静态成员函数和非静态成员函数不会影响对象内存的大小。静态成员变量存储在内存的静态数据区。另外:非静态成员函数和程序普通函数的函数区别是,类的非静态成员函数函数有this指针,所以可以访问类的成员。静态成员函数不可以调用类的非静态成员,因为静态成员函数不含this指针。
下面用实验说明:
class simpleClass
{
public:
static int nCount;
double nValue;
char c,d;
simpleClass(){};
virtual ~simpleClass(){};
int getValue(void);
virtual void foo(void){};
static void addCount(){};
};
int main()
{
simpleClass aSimple;
printf("Object start address: %x\n", &aSimple);
printf("nValue address: %x\n", &aSimple.nValue);
printf("c address: %x\n", &aSimple.c);printf("c address: %x\n", &aSimple.d);
printf("size: %d\n", sizeof(simpleClass));getchar();return 0;
}
输出:
Object start address:1efdec
nValue address: 1efdf4
c address:1efdfc
d address: 1efdfd
size: 24
因为double是8个字节,所以虚函数表和两个char的的成员都按照8字节对齐,所以总大小是24。
8字节 | vptr 虚函数指针 |
8字节 | double nValue |
8字节 | char c,d |
二:单继承
先举例
class simpleClass
{
public:
static int nCount;
int nValue;
char c;
simpleClass(){};
virtual ~simpleClass(){};
int getValue(void);
virtual void foo(void){};
static void addCount(){};
};
class derivedClass:public simpleClass
{
public:
int nSubValue;
derivedClass(){};
~derivedClass(){};
virtual void foo(void){};
};
int main()
{
derivedClass aSimple;
printf("Object start address: %x\n", &aSimple);
printf("nValue address: %x\n", &aSimple.nValue);
printf("c address: %x\n", &aSimple.c);
printf("nSubValue address: %x\n", &aSimple.nSubValue);
printf("size: %d\n", sizeof(derivedClass));
getchar();
return 0;
}
输出:
Object start address:23fa5c
nValue address: 23fa60
c address:23fa64
nSubValue address: 23fa68
size: 16
可见:
在构造一个派生类的实例时首先够着一个基类的实例,而这个基类的实例在派生类的实例销毁之后销毁。derivedClass的大小是16字节,基类simpleClass的大小是12字节。派生类增加了一个整型变量nSubValue,在内存布局中放在基类的后面。而且派生类在构造时不会再创建一个新的虚函数表,而是在基类的虚函数表中增加或者修改。三:多继承-简单
- 与但继承相同,创建派生类的对象时,要首先创建基类的对象。由于多继承中一个派生类有多个基类,因此创建基类的对象要遵循派生类声明的顺序。
- simpleClass1 的一个对象的大小是12字节,simpleClass2的一个对象的大小是8字节。而派生类增加了4个字节的整型成员数据,大小是24字节。
- 在多继承中药注意避免二义性。如上面的例子两个基类都定义了getValue()函数。如果一个派生类的实例调用getValue函数会报二义性错误。
- 若simpleClass1和simpleClass2的内存对齐不一样,那么派生类继承他们以后,会重新统一内存对齐。比如若simpleClass1是按4字节对齐,simpleClass2是按8字节对齐。当derivedClass继承他们以后,simpleClass1也会按照8字节对齐。
class simpleClass1
{
public:
int nValue1;
char c;
simpleClass1(){};
virtual ~simpleClass1(){};
int getValue(void);
virtual void foo1(void){};
};
class simpleClass2
{
public:
int nValue2;
simpleClass2(){};
virtual ~simpleClass2(){};
int getValue(void);
virtual void foo2(void){};
};
class derivedClass:public simpleClass1,public simpleClass2
{
public:
int nSubValue;
derivedClass(){};
~derivedClass(){};
virtual void foo2(void){};
};
int main()
{
derivedClass aSimple;
printf("Object start address: %x\n", &aSimple);
printf("nValue1 address: %x\n", &aSimple.nValue1);
printf("c address: %x\n", &aSimple.c);
printf("nValue2 address: %x\n", &aSimple.nValue2);
printf("nSubValue address: %x\n", &aSimple.nSubValue);
printf("size: %d\n", sizeof(aSimple));
getchar();
return 0;
}
输出:
Object start address:1bfe04
nValue1 address: 1bfe08
c address:1bfe0c
nValue2 address: 1bfe14
nSubValue address: 1bfe18
size: 24
四:多继承-菱形继承(使用虚继承)
考虑如下的菱形继承
如果不适用虚拟继承,内存布局将会如上面右图所示:会有两个baseClass实例。
如果使用虚拟继承,则内存布局如下
使用虚拟继承以后
- baseClass只创建了一个实例,其数据成员的地址相同
- baseClass的实例放在derivedClass实例的内存空间中的最后
- 为了使用虚拟继承,每个使用虚拟继承的类都会添加一个虚基类表的指针(virtual base table)来实现,因此照成空间变大。