c++中虚析构函数如何实现多态的、内存布局如何?
作者:冯Jungle
链接:https://www.zhihu.com/question/36193367/answer/2242824055
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
之前Jungle写过一篇文章《探究C++:虚函数表究竟怎么回事?》,主要通过测试代码来验证虚函数表的存在,进而说明C++的多态机制。但完成文章后仍旧觉得文章云里雾里,并不能很好地说明C++类的内存布局。于是在阅读完3遍《深度探索C++对象模型》之后,重新整理了相关知识点,完成此文。C++类在有无继承、有无虚函数、有无多重继承或者虚继承时,其内存布局是不一样的。本文将分别阐述各种case。1. 无继承1.1. 无虚函数示例代码如下:class A
{
private:
short pri_short_a;
public:
int i_a;
double d_a;
static char ch_a;
void funcA1() {}
};A的大小及布局如下:<img src="https://pic1.zhimg.com/50/v2-415a76e4be5b7dd0e1c97c2e40cbc011_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="117" data-original-token="v2-16c723469c04c5a93716106c46413f21" class="origin_image zh-lightbox-thumb" width="554" data-original="https://picx.zhimg.com/v2-415a76e4be5b7dd0e1c97c2e40cbc011_r.jpg?source=1940ef5c"/>如上可以说明:静态数据成员虽然属于类,但不占用具体类对象的内存。成员函数不占用具体类对象内存空间,成员函数存在代码区。数据成员的访问级别并不影响其在内存的排布和大小,均是按照声明的顺序在内存中有序排布,并适当对齐。1.2. 有虚函数在1.1中的类A里增加一个虚函数:class A
{
private:
short pri_short_a;
public:
int i_a;
double d_a;
static char ch_a;
void funcA1() {}
virtual void funcA2_v();
};其内存大小及布局如下:<img src="https://pica.zhimg.com/50/v2-e5b2e5f04896923770bfb2c91cc6414c_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="137" data-original-token="v2-70381f15885a081492e83cb335983292" class="origin_image zh-lightbox-thumb" width="554" data-original="https://pica.zhimg.com/v2-e5b2e5f04896923770bfb2c91cc6414c_r.jpg?source=1940ef5c"/>可以看到,A的起始处存储的是虚指针vptr,指针大小是4字节,这里是为了对齐8字节。为方便观察,之后的讨论中,我们统一把数据成员都改为int类型,占4字节。现在我们再加一个虚函数funcA_v2():class A
{
private:
short pri_short_a;
public:
int i_a;
double d_a;
static char ch_a;
void funcA1() {}
virtual void funcA2_v1();
virtual void funcA2_v2();
};布局如下:<img src="https://picx.zhimg.com/50/v2-643e17b464f6f71f4dd7bb5cf2bb23f2_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="245" data-original-token="v2-a318a1e46145e556334082fbecd4f84d" class="origin_image zh-lightbox-thumb" width="554" data-original="https://picx.zhimg.com/v2-643e17b464f6f71f4dd7bb5cf2bb23f2_r.jpg?source=1940ef5c"/>所以,不论再多虚函数,都只会有一个虚指针vptr,不会改变类的大小。不同之处在于,虚指针所指向的虚表中会多一个项目,即指向另一个虚函数的地址。2. 单一继承2.1. 单一继承且无虚函数如下,我们设计了类A、B和C,其中,B继承自A,C继承自B:class A
{
public:
int i_a;
static char ch_a;
void funcA1() {}
};
class B : public A
{
public:
int i_b;
void funcB1() {}
};
class C :public B
{
public:
int i_c;
};内存布局如下:<img src="https://picx.zhimg.com/50/v2-57f57ec3edbebf7e55374bb4954cd477_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="413" data-original-token="v2-ecfbd04d4182ca487760cce87715b750" class="origin_image zh-lightbox-thumb" width="554" data-original="https://picx.zhimg.com/v2-57f57ec3edbebf7e55374bb4954cd477_r.jpg?source=1940ef5c"/>单一继承的内存布局很清晰,每个派生类中起始位置都是Base class subobject。现在我们在类中增加虚函数,观察在单一继承+有虚函数的情况下,类的内存布局。2.2. 单一继承且有虚函数如下:类A增加了两个虚函数funcA_v1()和funcA_v2()类B继承自A,覆写funcA_v1()类C继承自B,重写funcA_v1(),且有自己定义的一个虚函数funcC_v1()class A
{
public:
int i_a;
static char ch_a;
void funcA1() {}
virtual void funcA_v1();
virtual void funcA_v2();
};
class B : public A
{
public:
int i_b;
void funcB1() {}
virtual void funcA_v1();
};
class C :public B
{
public:
int i_c;
virtual void funcA_v1();
virtual void funcC_v1();
};Class A的内存布局如下,如同1.2,这里不再解释:<img src="https://pic1.zhimg.com/50/v2-8d74712d5ae07df077776086027a0b5c_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="244" data-original-token="v2-e6dd3146fde896779afb5d2f8591dd0b" class="origin_image zh-lightbox-thumb" width="554" data-original="https://pica.zhimg.com/v2-8d74712d5ae07df077776086027a0b5c_r.jpg?source=1940ef5c"/>Class B的内存布局如下:<img src="https://picx.zhimg.com/50/v2-0d932f7cb65c07e7a0386b64301e22e8_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="281" data-original-token="v2-59687fb6644f2c784dd8062942005292" class="origin_image zh-lightbox-thumb" width="554" data-original="https://picx.zhimg.com/v2-0d932f7cb65c07e7a0386b64301e22e8_r.jpg?source=1940ef5c"/>B中首先也是基类A subobject,同样含有一个虚指针vptr。由于B覆写了funcA_v1(),故虚表中第一个索引处的函数地址是&B::funcA_v1()。理解了B的内存布局,接下来C的内存布局也就不必赘述:<img src="https://pic1.zhimg.com/50/v2-fbd3236dd2689a83aae7423636908ca0_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="366" data-original-token="v2-2d23bf240f7327bbc9548400effb1f46" class="origin_image zh-lightbox-thumb" width="554" data-original="https://pica.zhimg.com/v2-fbd3236dd2689a83aae7423636908ca0_r.jpg?source=1940ef5c"/>必须要提及两点:虚析构函数和覆写。虚析构函数在B.3.中详述。怎么才算是覆写?——类的继承里,子类里含有与父类里同名的虚函数,函数名、函数返回值类型和参数列表必须相同,权限可以不同。如上面示例中,B和C都覆写了A的funcA_v1()。下面的例子说明了这一点:<img src="https://picx.zhimg.com/50/v2-8c64656148d570fdc8ea4574f777aeaa_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="155" data-original-token="v2-a68117e364551f3ad1808ea0b06db5e4" class="origin_image zh-lightbox-thumb" width="554" data-original="https://picx.zhimg.com/v2-8c64656148d570fdc8ea4574f777aeaa_r.jpg?source=1940ef5c"/><img src="https://pic1.zhimg.com/50/v2-b98d7d124ebbf06dde61b20eb07741f6_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="407" data-original-token="v2-99881d36c56bb9e83d61a302b9e63671" class="origin_image zh-lightbox-thumb" width="554" data-original="https://picx.zhimg.com/v2-b98d7d124ebbf06dde61b20eb07741f6_r.jpg?source=1940ef5c"/>2.3. 虚析构函数《Effective C++》第三版,Item 07:为多态基类声明virtual析构函数。 当一个派生类对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁。所以上述的类设计其实有错误,带多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。 在接下来的示例中,我们将加上虚析构函数。3. 多重继承3.1. 多重继承<img src="https://picx.zhimg.com/50/v2-0b161c389ec15ae3cb4364025cccd1d4_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="283" data-rawheight="131" data-original-token="v2-373395ecbe0514c18b613aa1f6606146" class="content_image" width="283"/>如下是一个简单的继承关系,class C同时继承自A和B:class A
{
public:
int i_a;
void funcA1() {}
virtual ~A() {}
};
class B
{
public:
int i_b;
void funcB1() {}
virtual ~B() {};
};
class C :public A, public B
{
public:
int i_c;
virtual ~C() {}
};类A和B的内存布局如同1.2。而类C的内存布局如下:<img src="https://pica.zhimg.com/50/v2-424c48268f4fda4d977d93c132c9cd4f_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="438" data-original-token="v2-57041d84845f803cae82eebe452e0512" class="origin_image zh-lightbox-thumb" width="554" data-original="https://pica.zhimg.com/v2-424c48268f4fda4d977d93c132c9cd4f_r.jpg?source=1940ef5c"/>可见,派生类C中依其继承的基类的顺序,存放了各个基类subobject及各自的vptr,然后才是Class C自己的数据成员。需要解释上图中的thunk:Thunk解释:所谓thunk是一小段assembly代码,用来(1)以适当的offset值调整this指针,(2)跳到virtual function去。例如,经由一个Base2指针调用Derived destructor,其相关的thunk可能看起来是下面这个样子://虚拟C++代码
pbase2_dtor_thunk:
this += sizeof( base1 );
Derived::~Derived( this );根据上面的解释,经由class A的指针调用C的析构函数,其offset等于0;而经由class B调用C的析构函数,其offset等于8,如同上图所示:this-=8。同时也可以想到,随着base class的数量增多,派生类里也会首先顺序存放各个基类subobject。而派生类中也会记录其到各个base subobject的offset。如下图是类D同时继承类A、B、C:<img src="https://pica.zhimg.com/50/v2-6952e026c8ae34630e35cd9275f80691_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="574" data-original-token="v2-834e5359acfebf9eaa2fbc83d0f7dee5" class="origin_image zh-lightbox-thumb" width="554" data-original="https://pica.zhimg.com/v2-6952e026c8ae34630e35cd9275f80691_r.jpg?source=1940ef5c"/>3.2. 菱形继承<img src="https://pic1.zhimg.com/50/v2-9aa334f6675d70300d2b6039912b1f02_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="342" data-rawheight="190" data-original-token="v2-a71b0c282c0989d90010aea99692e678" class="content_image" width="342"/>如上图是一个菱形继承的示意图,类B和C均继承自类A,类D同时继承类B和C,代码如下:class A
{
public:
int i_a;
virtual ~A() {}
};
class B :public A
{
public:
int i_b;
virtual ~B() {};
};
class C :public A
{
public:
int i_c;
virtual ~C() {}
};
class D :public B, public C
{
public:
int i_d;
virtual ~D() {}
};类A的内存布局很简单,如1.2。类B和C的内存布局如2.2。接下来看类D的内存布局:<img src="https://picx.zhimg.com/50/v2-cc5bd8d055ea3777ee9353918d58183f_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="542" data-original-token="v2-d345c1e393f0e638997149608b48f986" class="origin_image zh-lightbox-thumb" width="554" data-original="https://pic1.zhimg.com/v2-cc5bd8d055ea3777ee9353918d58183f_r.jpg?source=1940ef5c"/>如上图,D中依次存放基类B subobject和基类C subobject。其中B和C中均存放一份class A subobject。3.3. 虚拟继承从菱形继承的most-derived class(即3.2.中的class D)的内存布局可以看出,subobject A有两份,所以A的data member也存了两份,但实际上对于D而言,只需要有一份subobject A即够了。菱形继承不仅浪费存储空间,而且造成了数据访问的二义性。虚拟继承可以很好地解决这个问题。同样以3.2.中的继承关系为例,不过这次我们B和C对A的继承都加上了关键字virtual。<img src="https://picx.zhimg.com/50/v2-81d3a81a14cfb8ac20b107297cc2cf82_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="376" data-rawheight="205" data-original-token="v2-3d0214a9316e6046d85495b090778ef9" class="content_image" width="376"/>class A
{
public:
int i_a;
virtual ~A() {}
};
class B :virtual public A
{
public:
int i_b;
virtual ~B() {};
};
class C :virtual public A
{
public:
int i_c;
virtual ~C() {}
};
class D :public B, public C
{
public:
int i_d;
virtual ~D() {}
};接下来看看各个类的内存布局。A的内存布局同1.2。类B和C的内存布局如2.2?是吗?不是!如下图:<img src="https://picx.zhimg.com/50/v2-dcaed0602832c04b46e0d1906d38f6d9_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="400" data-original-token="v2-94309a7dd43773965d844b9c2b620169" class="origin_image zh-lightbox-thumb" width="554" data-original="https://picx.zhimg.com/v2-dcaed0602832c04b46e0d1906d38f6d9_r.jpg?source=1940ef5c"/>可以看到,class B中有两个虚指针:第一个指向B自己的虚表,第二个指向虚基类A的虚表。而且,从布局上看,class B的部分要放在前面,虚基类A的部分放在后面。在class B中虚基类A的成分相对内存起始处的偏移offset等于class B的大小(8字节)。C的内存布局和B类似。这个布局与之前的不一样:为什么基类subobject反而放到后面了?Class如果内含一个或多个virtual base subobjects,将被分割成两部分:一个不变区域和一个共享区域。不变区域中的数据,不管后继如何衍化,总有固定的offset(从object的开头算起),所以这一部分可以直接存取。而共享区域所表现的就是virtual base class subobject。这部分数据的位置会因为每次的派生操作而发生变化,所以它们只可以被间接存取。接下来看class D的内存布局:直接的基类B和C按照声明的继承顺序,在D的内存中顺序安放。紧接着是D的data member。然后是共享区域virtual base class A。<img src="https://pic1.zhimg.com/50/v2-e10accb704767bcab5b5a89f7cfe2044_720w.jpg?source=1940ef5c" data-caption="" data-size="normal" data-rawwidth="554" data-rawheight="592" data-original-token="v2-6c358e8a7883976586048cf6862c1eb7" class="origin_image zh-lightbox-thumb" width="554" data-original="https://pic1.zhimg.com/v2-e10accb704767bcab5b5a89f7cfe2044_r.jpg?source=1940ef5c"/>总结可以看到,C++类在有无继承、有无虚函数、有无多重继承或者虚继承时,其内存布局大不一样,多重继承或者菱形继承下,内存布局甚至很复杂。大致理清之后,可以对C++类的内存布局有个清晰认识。
https://www.zhihu.com/question/36193367/answer/2242824055