1:前言
此文纯属个人业余研究所得,所有理论均来自书籍但经过vs2012验证通过。写此文的目的是为了更好地了解C++对象的内存布局,也算是C++学习历程上的一个新台阶。如若有理论或者验证上的错误之处,请留言!
2:C++内存对象布局的影响因素
C++是强类型语言,因此对某块内存是根据指定类型进行解析的,包括build-in类型及自定义类型。这里我们着重研究自定义类型,也就是用class定义的类型。
影响内存对象布局的因素可归类为以下几种:(1)本类中是否定义有虚函数;(2)本类是否继承自其他类及继承的类型。
3:各种情况下的内存布局分析
本节中所有代码及运行结果均来自vs2012。
3.1:不带虚函数时单一类的内存布局
定义以下类:
class A
{
private:
int m1;
int m2;
public:
A(){ m1 = 1; m2 = 2; }
};
int main()
{
A a;
int* p = (int*)&a;
cout << "Object First Address: " << p << endl;
cout << "Object's Size is: " << sizeof(a) << endl;
cout << p << ":" << *p << endl;
cout << ++p << ":" << *p << endl;
return 0;
}
其运行结果如下:
<pre name="code" class="cpp">Object First Address: 0035FD04
Object's Size is: 8
0035FD04:1
0035FD08:2
Press any key to continue . . .
从这里可以看出,对象的内存布局只是包含了它的成员变量(涉及到内存对齐),而不包含它所定义的成员函数。各类的成员变量按照定义的顺序在内存对齐的规则下依次连续分布在内存中
在后续章节中我们会看到虚函数的实现机制给对象的内存布局带来的影响。
</pre><p>3.2:带虚函数时的单一类的内存布局</p><p>测试代码如下:</p><p></p><pre name="code" class="cpp">class A
{
private:
int m1;
int m2;
public:
A(){ m1 = 1; m2 = 2; }
virtual int V();
};
int A::V()
{
cout << "A::V()" << endl;
return 0;
}
int main()
{
A a;
int* p = (int*)&a;
cout << "Object First Address: " << p << endl;
cout << "Object's Size is: " << sizeof(a) << endl;
typedef int (*pFunc)();
pFunc func = (pFunc)*(int*)*p;
func();
cout << ++p << ":" << *p << endl;
cout << ++p << ":" << *p << endl;
return 0;
}
其运行结果如下:
Object First Address: 002FFDE4
Object's Size is: 12
A::V()
002FFDE8:1
002FFDEC:2
Press any key to continue . . .
从这里可以看出,在多定义了一个虚成员函数后,为了实现对虚函数的寻址,在内存对象的头部增加了一个指向虚函数地址的指针,该指针占4字节,故整个对象的大小为12字节。
在实验进行到此,可以得出几个结论:
一,实现类的访问权限控制在发生在编译阶段,而在运行阶段可以用指针直接访问对象的任何成员变量,包括private。
二,所有虚函数的地址均被存放在一张称为虚表的线性地址结构中,并且此表在编译时生成。与类相关,每个具体对象只需要增加保存指向此表的指针即可。
3.3:不带虚函数时单一继承时的内存布局
测试代码:
class A
{
private:
int m1;
int m2;
public:
A(){ m1 = 1; m2 = 2; }
};
class B : public A
{
private:
int B1;
int B2;
public:
B(){ B1 = 3; B2 = 4; }
};
int main()
{
B b;
int* p = (int*)&b;
cout << "Object's First Address: " << p << endl;
cout << "Object's Size is: " << sizeof(b) << endl;
cout << p << ": " << *p << endl;
cout << ++p << ": " << *p << endl;
cout << ++p << ": " << *p << endl;
cout << ++p << ": " << *p << endl;
return 0;
}
其运行结果如下:
Object's First Address: 002EF8FC
Object's Size is: 16
002EF8FC: 1
002EF900: 2
002EF904: 3
002EF908: 4
Press any key to continue . . .
结论如下:
C++对继承的实现方式是基于父类的成员再加上自己的成员(包括父类的private成员)。在内存对齐上,将父类的成员纳入自己的成员一起考虑对齐值。
3.4:带虚函数时单一继承时的内存布局
测试代码:
class A
{
private:
int m1;
int m2;
public:
A(){ m1 = 1; m2 = 2; }
virtual int VA();
virtual int V();
};
int A::VA()
{
cout << "A::VA()" << endl;
return 0;
}
int A::V()
{
cout << "A::V()" << endl;
return 0;
}
class B : public A
{
private:
int B1;
int B2;
public:
B(){ B1 = 3; B2 = 4; }
virtual int VB();
virtual int V();
};
int B::VB()
{
cout << "B::VB()" << endl;
return 0;
}
int B::V()
{
cout << "B::V()" << endl;
return 0;
}
int main()
{
B b;
cout << "Object's First Address: " << &b << endl;
cout << "Object's Size is: " << sizeof(b) << endl;
//invoke virtual method
typedef int (*pFunc)();
int** pp = (int**)&b;
for (int i = 0; i < 3; i++)
{
pFunc func = (pFunc)pp[0][i];
func();
}
cout << ++pp << ":" << *(int*)pp << endl;
cout << ++pp << ":" << *(int*)pp << endl;
cout << ++pp << ":" << *(int*)pp << endl;
cout << ++pp << ":" << *(int*)pp << endl;
return 0;
}
运行结果如下:
Object's First Address: 0021F8E0
Object's Size is: 20
A::VA()
B::V()
B::VB()
0021F8E4:1
0021F8E8:2
0021F8EC:3
0021F8F0:4
Press any key to continue . . .
分析运行结果可得知:
一,当父类含有虚函数时,普通的继承体系中子类的虚函数表中会继承父类的虚函数。对于虚函数的排序与成员的顺序是一致的,但是有一点必须注意,那就是当子类是对父类同名虚函数的重写时,子类的虚函数位置会调整到父类的同名虚函数位置处。这也是为了在调用子类的虚函数时不会调用到父类的虚函数而实现的。
二,普通的继承时,对象中只需要增加一个指向虚表的指针即可实现所有的C++特性,故这里的大小为20,只是比不含有虚函数时的继承增加了一个4字节指针。
3.5 菱形继承结构时的内存布局
在普通的继承体系中,子类会把父类的所有成员会继承过来并体现在内存对象的布局中。这样的实现方式却在含有相同超父类时导致了二义性,为了解决这个问题,C++中引入了虚继承的概念。在虚继承的体系中,内存的布局也不再是普通继承那样简简单单地继承父类的所有成员变量
先来看一下只有两层结构时虚继承内存布局,测试如下:
class A
{
private:
int m1;
int m2;
public:
A(){ m1 = 1; m2 = 2; }
};
class B : virtual public A
{
private:
int B1;
int B2;
public:
B(){ B1 = 3; B2 = 4; }
};
int main()
{
B b;
cout << "Object's size: " << sizeof(b) << endl;
int* p = (int*)&b;
//本类的首地址与此指针位置的offset
cout << p << " : " << *(int*)*p << endl;
int* po = (int*)*p;
//本类虚基类与此指针位置的offset
cout << po << " : " << *++po << endl;
cout << ++p << " : " << *p << endl;
cout << ++p << " : " << *p << endl;
cout << ++p << " : " << *p << endl;
cout << ++p << " : " << *p << endl;
return 0;
}
运行结果:
Object's size: 20
0028F8C0 : 0
00ACCD28 : 12
0028F8C4 : 3
0028F8C8 : 4
0028F8CC : 1
0028F8D0 : 2
Press any key to continue . . .
结果分析:
一,这里的继承属于单一继承,唯一的不同点在于继承时多了一个关键字virtual。虚继承时比普通继承时的内存对象多了4个字节。
二,虚基类的成员被放到最下面,而不再是普通继承体系中的最上面。
那么问题来了,为什么会增加4个字节,这4个字节的用处是什么?为什么虚继承时虚基类的成员要放在最下面,与普通的继承相反呢?
回答第一个问题:对虚函数的实现各个厂商的编译器大都大同小异,但在实现虚继承方面却差异很大。本文是基于vs2012进行测试的,其增加的4个字节本质上是一个指针,该指针指向的是一个被称为虚基类表的结构,该表不同于虚函数表,它的第一项存放的是当前类的首地址与指针所在地址之间的偏移值,如果本类含有虚函数,那首地址是一个指向虚表的指针,那么这时的值应该是-4,如果不含有虚函数,那些指向虚基类表的指针就是对象的每一个数据项,此时的值应该是0 。这正是本例测试的情况。它的第二项则是存放当前类的虚基类数据的首地址与此指针所在地址之间的偏移值。这里由于在些指针及两个子类的int成员之后者该类的虚基类的成员数据,故这里存放的是数值为12。
回答第二个问题:其实我感觉放上而不这是放下面都可以,就看编译器是怎么实现的了。
接下来叙述一下含有虚函数时的继承情况,其实含有虚函数的情况与这里大体相似,只是多了虚函数的在虚表中的位置分布而已,而虚函数的分布与继承时是一样,若有同名的虚函数则会覆盖,没有则加上当前虚函数的后面,对于多继承中的grandson的新增加的虚函数,则增加在每一个对象的虚函数后面,这样做是为了能够直接调用grandson类的新增加的虚函数。