首页 > 系统相关 >c++中虚析构函数如何实现多态的、内存布局如何?

c++中虚析构函数如何实现多态的、内存布局如何?

时间:2023-06-23 10:11:43浏览次数:46  
标签:函数 int 内存 多态 virtual class c++ public 中虚析构

作者:冯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

 

标签:函数,int,内存,多态,virtual,class,c++,public,中虚析构
From: https://www.cnblogs.com/im18620660608/p/17498766.html

相关文章

  • c++ 64位输出地址超过int类型使用longlong类型输出
    #include<iostream>usingnamespacestd;intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,20};cout<<"整个数组所占内存空间为"<<sizeof(arr)<<endl;cout<<"每个元素所占内存空间为"<<sizeof(arr[0])<<endl;cout<<"......
  • C++面试八股文:override和finial关键字有什么作用?
    某日二师兄参加XXX科技公司的C++工程师开发岗位第22面:(二师兄好苦逼,节假日还在面试。。。)面试官:C++的继承了解吗?二师兄:(不好意思,你面到我的强项了。。)了解一些。面试官:什么是虚函数,为什么需要虚函数?二师兄:虚函数允许在基类中定义一个函数,然后在派生类中进行重写(override)。二......
  • C++面试八股文:什么是左值,什么是右值?
    C++面试八股文:什么是左值,什么是右值?某日二师兄参加XXX科技公司的C++工程师开发岗位第16面:面试官:什么是左值,什么是右值?二师兄:简单来说,左值就是可以使用&符号取地址的值,而右值一般不可以使用&符号取地址。inta=42; //a是左值,可以&aint*p=&a;int*p=&42; //42是右......
  • UE5 C++ TSet
    概念TSet是一种快速容器类,用于在排序不重要的情况下存储唯一元素TSet也是值类型,支持常规复制、赋值和析构函数操作,以及其元素较强的所有权TSet被销毁时,其元素也将被销毁。键类型也必须是值类型创建TSet<FString>FruitSet;添加元素Add将提供键加入setFruitSet.......
  • UE5 C++ TMap
    概述映射的元素类型为键值对,元素类型实际上是TPair<KeyType,ElementType>,只将键用于存储和获取TMap和TMultiMap两者之间的不同点是TMap中的键是唯一的,而TMultiMap可存储多个相同的键TMap是散列容器,这意味着键类型必须支持GetTypeHash函数,并提供运算符==来比较各个键是否......
  • C++入门教程
    C++入门教程----------------------------------------------------------一.初识C++---------------------------------------------------------1.什么是C++.c++是一种较为基础的编程语言,虽然没有Python,Scratch那么高级,但是它应用范围很广.不论是信息奥赛还是国......
  • UE5 C++ TArray
    概述TArray是UE4中最常用的容器类。其速度快、内存消耗小、安全性高TArray类型由两大属性定义:元素类型和可选分配器元素类型是存储在数组中的对象类型。TArray被称为同质容器。换言之,其所有元素均完全为相同类型。单个TArray中不能存储不同类型的元素。分配器常被省略,默......
  • Ts中的多态
    //父类定义一个方法不去实现,让继承它的子类去实现,每一个子类有不同的表现多态属于继承classAnimal{   name:string   constructor(name:string){       this.name=name   }   eat(){       console.log('吃的方法')   }}class......
  • c++ doctest 测试报告
    地址https://github.com/laolang2016/doctest-reports效果未完成事项状态筛选没做只做了dashboard,其他两个页面还是空白四栏确实有点多了,数据应该纵向展示jquery果然还是比较墨迹,下一个版本使用vue......
  • Delete vector contents and free up memory in C++
     DeletevectorcontentsandfreeupmemoryinC++Thispostwilldiscusshowtodeletethevector’scontentsandfreeupthememoryallocatedbythevectortostoreobjectsinC++.1.Using vector::clear functionWecanusethe vector::clear......