C++ 对象内存布局和多态实现原理

进入主题前,先把工具设置好。本文使用编译测试环境:Visual Studio 2013

VS2013查看类内存布局设置方式如下截图:



先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。


进入主题:从最基础的class到继承、多重继承、虚拟继承,我们依次展开来看。

1.空类

定义一个空类,如下:

class Base {

};
编译一下,可以看到输出框里面的布局:


发现空类Base的size是1,原因:C++标准规定,编译器为空类插入1字节的char,以使该类对象在内存得以配置一个地址。

2.普通类(只有nonstatic的成员变量

定义如下:

class Base {
private:
	int a;
	int b;
};
内存布局:


这里暂不考虑字节对齐的影响,所以成员变量都设为int型。从这里可以看到普通类的布局方式,nonstatic成员变量依据声明的顺序进行排列(类内偏移为0开始)。

当涉及字符对齐时的内存布局图。

定义如下:

class Base {
private:
	int a;
	char b;
};
内存布局:


从上图红色框图中,看到char b会被4byte对齐,Base object size是8,而不是5。

当在继承体系中,父类和子类都涉及字节对齐时,我们来看下内存布局状况。

定义如下:

class Base {
private:
	int a;
	char b;
};

class Derived : public Base{
private:
	int c;
	char d;
};
内存布局:


从上图红色框图中,看到Base suboject char b和Derived char d 都被4byte对齐,Derived object size是16。int c的内存并不是从位置5开始,而是从8开始。在继承中char b依然会如Base对象中被4byte对齐处理。这是因为:继承关系中派生类会保持基类subobject的内存布局完整性。

3.普通类(加上static成员和普通function

定义如下:

class Base {
public:
	void f();
	static void g();
private:
	int a;
	static int c;
	int b;
};
内存布局:


可以看出:static成员变量、static成员函数,普通成员函数,都不会占用对象的空间。static成员属于class,所有object共用一份。

4.普通类(加上虚函数

定义如下:

class Base {
public:
	void f();
	virtual void g();
private:
	int a;
	int b;
};
内存布局:

这个结构图分成上下两部分,上面是内存分布,下面是虚函数表vftable(也称作vtbl)。我们看到class Base size现在是12,除去两个int变量占用8个Byte,虚函数表指针vfptr(也称作vptr)占用4个Byte(32bit 机器指针size是4Byte)。VS2013编译器是把vfptr放在了内存的开始处(0地址偏移),然后再是两个依次声明的int型成员变量;下面生成了虚函数表,紧跟在&Base_meta后面的0表示,指向这张虚函数表的虚函数表指针vfptr在内存中的位置,紧接着下面列出了虚函数,左侧的0是这个虚函数的序号,这里只有一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数依次列出来。

5.单一的一般继承(只有nonstatic成员变量)

定义如下:

class Base {
private:
	int a;
	int b;
};

class Derived : public Base {
private:
	int c;
	int d;
};
内存布局:


单一的一般继承内存布局很简单,继承关系中派生类保持基类subobject的内存布局完整性,Derived的成员变量c & d 紧接在Base subobject之后,并按照声明顺序依次排列布局。

6.单一的一般继承(Derived带虚函数)

定义如下:

class Base {
private:
	int a;
	int b;
};

class Derived : public Base {
public:
	virtual void f();
private:
	int c;
	int d;
};
内部布局:


从上图看到,Derived带虚函数后,Derived object内存布局起始位置被编译器安插一个vfptr(指向vftable),vftable存储的是虚函数地址。vfptr之后的内存布局和单一的一般继承(Derived不带虚函数)一致。

7.单一的一般继承(带成员变量、虚函数、虚函数覆盖)

定义如下:

class Base {
public:
	void f();
	virtual void g();
	virtual void h();
private:
	int a;
	int b;
};

class Derived : public Base {
public:
	virtual void g();
	virtual void h_derived();
private:
	int c;
	int d;
};
Base的内存布局:


Derived的内存布局:


这个例子中,Base含有virtual f() 和 virtual g(),Derived覆写了Base的virtual g(),继承了Base的virtual h(),增加了virtual h_derived()。

我们先看Base的内存布局:vfptr在Base内存offset 0位置,紧接着是两个int行变量,vftable中就是Base定义的两个virtual函数g()和h()的地址。

再来分析Derived的内存布局:从上下两张内部布局图中(Base和Derived)可以看出,继承关系中派生类会保持基类subobject的内存布局完整性(Derived内存布局图中红色框内),vfptr还是处于内部布局offset 0位置(也是base class的内部布局offset 0位置),Derived中并没有额外再生成新的vfptr。但此时vfptr指向的的vftable中有三个虚函数,序号0是Derived中的覆写g(),序号1是继承自Base的虚函数h(),序号2是Derived中的新增virtual h_derived()。

编译器是如何利用vfptr与vftable来实现多态的呢?当创建一个含有虚函数的父类的对象时,编译器在对象构造时将vfptr指向父类的vftable;同样,当创建子类的对象时,编译器在构造函数里将vfptr指向子类的vftable(这个虚表里面的虚函数入口地址是子类的,包含有:继承来父类的虚函数,子类中覆写父类的虚函数,子类中扩展新增的虚函数)。所以,如果是调用Base *p = new Derived();父类指针p指向的内存起始位置是子类对象的内存起始位置,但父类指针只能访问父类内存范围内的成员。在new Derived()时,子类对象的vfptr指向的是子类的vftable,所以这时候通过父类指针p访问的虚函数其实是子类中的vftable中的虚函数,p->VirtualFunction,实际上是p->vfptr[序号]->VirtualFunction,这就是多态的实现原理。

8.单一的虚拟继承(带成员变量、虚函数、虚函数覆盖)

定义如下:

class Base {
public:
	void f();
	virtual void g();
	virtual void h();
private:
	int a;
	int b;
};

class Derived : virtual public Base {
public:
	virtual void g();
private:
	int c;
	int d;
};
内存分布:


从上图看到,虚拟继承,Derived object的内存起始位置,被编译器安插一个vbptr,vbptr是Derived中指向虚基类表(vbtable)的指针。

上图Derived::$vtable@看到序号1对应的数字12,是virtual base Base距离vbptr的偏移距离,也正是上图中virtual base Base下方vfptr在内存布局中的位置12。

9.单一的虚拟继承(带成员变量、虚函数、虚函数覆盖,派生类新增虚函数)

定义如下:

class Base {
public:
	void f();
	virtual void g();
	virtual void h();
private:
	int a;
	int b;
};

class Derived : virtual public Base {
public:
	virtual void g();
	virtual void h_derived();
private:
	int c;
	int d;
};
内存分布:


从上图与单一的虚拟继承(派生类不新增虚函数)相比,Derived object size是28,相比增大了4个byte,而这4个byte正是Derived中被编译器新增安插的vfptr(上图红色框图中)。我们看到Derived新增的虚函数h_derived并没有直接放入Derived::$vftable@Base@中,而是新增加vftable(Derived::$vftable@Derived@)。

10.多重继承

定义如下:

class Base {
public:
	virtual void virtualFunction();
private:
	int a;
	int b;
};

class Derived1 : public Base {
public:
	virtual void virtualFunction();
	virtual void virtualDerived1Function();
private:
	int c;
};

class Derived2 : public Base {
public:
	virtual void virtualFunction();
	virtual void virtualDerived2Function();
private:
	int d;
};

class Derived : public Derived1, public Derived2 {
public:
	virtual void virtualFunction();
private:
	int e;
};
内存布局(从父类到子类依次来看):


Base中有一个vfptr,地址偏移为0。


Derived1继承了Base,内存布局是先父类后子类。


Derived2内部布局和Derived1相同:继承了Base,内存布局是先父类后子类。


重点看这个类Derived,它按照继承顺序并列排布着继承而来的两个父类Derived1与Derived2,还有自身的成员变量e。Derived1包含了继承自父类的Base(Base有一个0地址偏移的vfptr,然后是成员变量a和b)以及它自身的成员变量c。Derived2的内存布局类似于Derived1。我们注意到Derived2里面也有一份Base,这就是C++多重继承中的二义性。

此外,我们看到这里有两份vftable,分别针对Derived1与Derived2。我们把上图中的关于vftable部分截取出来,如下图,分析下这两个vftable。


在靠上面的vftable@Derived1的&Derived_meta下方的数字0,表示的是指向这张虚函数表的vfptr在Derived内存布局中的偏移位置,这也正是Derived1中的vfptr在Derived的内存偏移位置。靠下面的vftable@Derived2的那个16表示指向这个vftable的vfptr在内存中的偏移,这也正是Derived2中的vfptr在Derived的内存偏移位置。

11.多重继承(派生类新增虚函数)

同8中的内定义相比,只是在派生类Derived新增虚函数:virtual void virtualDerivedFunction()

定义如下:

class Base {
public:
	virtual void virtualFunction();
private:
	int a;
	int b;
};

class Derived1 : public Base {
public:
	virtual void virtualFunction();
	virtual void virtualDerived1Function();
private:
	int c;
};

class Derived2 : public Base {
public:
	virtual void virtualFunction();
	virtual void virtualDerived2Function();
private:
	int d;
};

class Derived : public Derived1, public Derived2 {
public:
	virtual void virtualFunction();
	virtual void virtualDerivedFunction();
private:
	int e;
};
内存布局:


此case中派生类Derived和8中所讲的内存布局相比,我们看到唯一差异点:上图红色框图中,Derived新增的虚函数只有在vftable@Derived1的虚函数表中有,并不会在vftable@Derived2的虚函数表。也就是说Derived并没有额外新增vfptr和vftable,而是直接引用来自继承列表中的第一个父类Derived1中的vftable。

12.钻石型多重虚继承

为了解决多重继承可能带来的二义性,以及减少对基类的重复。C++从语言层面提供了虚继承,其代价是增加了虚表指针的负担(更多的虚表指针)。还是直接看例子。

定义如下:

class Base {
public:
	virtual void virtualFunction();
private:
	int a;
	int b;
};

class Derived1 : virtual public Base {
public:
	virtual void virtualFunction();
	virtual void virtualDerived1Function();
private:
	int c;
};

class Derived2 : virtual public Base {
public:
	virtual void virtualFunction();
	virtual void virtualDerived2Function();
private:
	int d;
};

class Derived : public Derived1, public Derived2 {
public:
	virtual void virtualFunction();
	virtual void virtualDerivedFunction();
private:
	int e;
};
内存分布(从父类到子类依次来看):


从上图看出父类Base内存布局没有发生变化。但往下看:


从上图中,我们看到Derived1的内存布局已经发生改变,相比原来非虚继承:非虚继承内部布局中是先排vfptr(vfptr位于0地址偏移处)与Base成员变量,再次接着才是Derived1的成员变量c。但现在有3个虚指针了,其中2个vfptr,另一个是vbptr,vbptr是Derived1中指向虚基类表(vbtable)的指针。

此外对比非虚继承,我们看到:如果是虚继承,则子类会新增一个指向vbtable的vbptr。

我们按照下图依次分析这3张虚表。

第1张表(Derived1::$vftable@Derived1@)是vfptr指向的vftable,&Derived1_meta下方的0表示vfptr在Derived1中的内存位置,紧接着下面的序号0对应的是Derived1新增的虚函数(virtual void virtualDerived1Function())的地址。

第2张表(Derived1::$vbtable@)是虚基类表vbtable,标号0对应的4表示vbptr在Derived1中的内存位置,紧接着下方标号1对应的8(Derived1d(Derived1+4)Base)是vbptr距离virtual base Base的偏移距离(也就是virtual base Base在Derived1中的内存位置12减去vbptr在Derived1中内存位置4)。

第3张表(Derived1::$vftable@Base@)是继承自Base的subobject中vfptr指向的vftable,12表示指向此vftable的vfptr在Derived1中的内存位置,紧接着的标号0对应的表示Derived1中覆写Base的虚函数的地址。


Derived2的内存布局类似于Derived1,同样会有3个虚指针,分别指向3张虚表(第二张是虚基类表)。内部布局如下图:


下面来看最复杂的Derived的内存分布,这里面有5个虚指针了,但Base却只有一份。

第1张(Derived::$vftable@Derived1@)vftable是内含Derived1的,&Derived_meta下方的0表示它的vfptr在Derived内存布局中的位置。

第2张(Derived::$vftable@Derived2@)vftable是内含Derived2的,12表示它的vfptr在Derived内存布局中的位置。

第3张(Derived::$vbtable@Derived1@)vbtable是内含Derived1的,标号0对应的4表示vbptr在Derived中的内存位置,标号1对应的24 (Derivedd(Derived1+4)Base)是base class Derived1中的vbptr距离virtual base Base的偏移距离(正是下图中virtual base Base在Derived中的内存位置28减去base class Derived1中的vbptr在Derived中的内存位置4)。

第4张(Derived::$vbtable@Derived2@)vbtable是内含Derived2的,标号1对应的12 (Derivedd(Derived2+4)Base)是base class Derived2中的vbptr距离virtual base Base的偏移距离(正是下图中virtual base Base在Derived中的内存位置28减去base class Derived2中的vbptr在Derived中的内存位置16)。

第5张(Derived::$vftable@Base@)vftable是Derived继承自Base的。28表示virtual class Base的vfptr在Derived内存布局中的位置。



最后总结:

1.当类含有虚函数时(包括继承而来的虚函数)都有虚函数表指针vfptr和虚函数表vftable。

2.单一的普通继承(非虚继承),子类只有一个vfptr(子类从父类继承下来),并指向自身的vftable(包含:继承自父类的虚函数、子类覆盖父类的虚函数、子类新增的虚函数)。

3.多重继承(非虚继承,可能存在多个的基类vfptr与vftable。如果子类有新增虚函数,编译器并不会为子类额外新增自己的vfptr,而是直接引用来自继承列表中的第一个父类中的vfptr,并在此vfptr指向的vftable中增加子类新增的虚函数。

4.如果是单一的虚拟继承,则子类对象中会被编译器安插一个指向vbtable(记录virtual base class在内存布局中与vbptr的距离偏移)的vbptr。当子类中有新增虚函数时,编译器则会给子类对象中再新增一个vfptr,这个vfptr指向新增vftable(这张vftable只含有子类自己新增的虚函数),而subobject中的vfptr指向的vftable(也就是virtual base class中的vfptr指向的vftable)中存储的是子类从父类继承或子类覆盖父类的虚函数地址。

5.对于钻石型多重继承,可以按照以上原则类推。


说明:C++对象内存布局,不同编译器实现细节不同,本文所有测试结果都依赖VS2013。

展开阅读全文

没有更多推荐了,返回首页