C++继承,多继承,虚继承,菱形继承的内存分布

一:无虚函数篇:

1.普通的继承(无虚函数):


在子类的开始的内存即为一个父类对象,然后接着的内存再是父类没有的成员,所以可以用父类的指针来指向子类,就是因为子类的开始地址为一个父类的对象。

反汇编子类的构造函数,可以发现:


会将子类的首地址作为父类的this指针传递并调用构造函数,然后再去对自身成员初始化工作,最后返回this指针。


反汇编子类的析构函数,可以发现:


先是子类析构函数 ---> 再是父类的析构函数调用


再看看为什么能将父类的指针指向子类:


实际上也就是将子类的首地址给父类指针,因为内存部分为父类在地址最开始处。


子类访问父类中的成员:

实际就是对一个this指针+偏移进行访问,无虚函数的父类第一个成员也就是[this]。


2,子类中有其他类的成员(无虚函数):

以这个为例:


内存结构同样是父类最前,其余成员按照声明顺序排序。

在构造函数中父类都是最先进行的,在这里就不多说,析构函数都是最后进行的。

再看构造函数不同的地方:


成员如果是类,则会按照声明顺序调用构造函数


再看析构函数不同地方:


析构函数会根据声明的类的相反顺序调用,同时通过this + 偏移来表示成员类的this指针



二,有虚函数篇:

定义三个类,Person和Chinese和Canadian:


查看内存地址可以知道:


继承的类的父类有虚函数时候就会在首地址加上一个vtablePtr(虚表指针),然后才是父类成员,子类成员。

虚表指针是什么,查看内存:

其实就是我们父类按照顺序所定义的虚函数的函数指针,虚表中的第一个成员为析构函数,第二成员为Speak函数和,如何用虚表来调用虚函数,以及如何构造函数和析构函数的变化又会有哪些,直接看反汇编代码吧。

和上面所说相同的就不管了,直接看不同的地方:

调用父类构造函数里面:

在init之前将,将父类的虚表地址赋给对象前四字节,此时的虚表指针指向父类的虚函数!,查看此时的虚表地址再查看这两个的函数指针指向的函数地址:



第一个函数即为默认的父类的析构函数,第二个函数为默认的Speak函数(什么都没干)。


执行完父类的构造函数只会,便会执行子类的构造函数部分:


仍然会先将虚表指针替换为子类的虚表指针,然后再进行子类Init操作,这时候再查看子类的虚表:



可以看到子类的析构代理函数和Speak函数已经将父类的给替换掉了。


再来看看析构函数:



可以发现,在子类和父类的析构函数中都会再次修改虚表指针为构造时候赋值的指针,这个原因是因为,当这个类进行再次继承时候就一定要修改虚表指针,否则子子类在构造函数调用虚函数就会调用到不是属于这个类的虚函数


构造和析构函数都会对虚表指针进行修改,那虚表指针中的虚函数是如何被调用,分析一下Speak函数的调用:

第一种情况是对象调用自己的虚函数,就如果ch.Speak()这种形式,看看反汇编代码:


直接就是以thiscall调用约定 call 一个函数地址。

第二种情况就是C++多态的实现,用父类指针指向子类,再通过父类指针进行调用子类的函数,Person *p = &ch:


可以发现C++的多态其实就是通过虚表指针来实现,父类指针通过访问子类虚表中的成员从而实现调用不同子类的虚函数,而虚表指针就是拿来索引得到虚函数地址的作用,这里就是可以解释为什么在构造函数或者析构函数中一定要对虚表指针进行赋值的原因,就是防止在析构或者构造函数中调用虚函数,如果不赋值则可能不能调用到本类中的虚函数,下来反汇编看看这种情况,分别在子类和父类的构造函数中加入Speak()的调用:


因为不是通过指针或引用调用Speak,而是直接在父类的构造函数里进行调用,这里虚表为父类的,所以直接就会CALL Speak进行调用。


但是当类中有一个接口函数,里面调用虚函数,这个时候在构造函数中调用这个接口函数又会怎么样:



查看接口函数:


可以看到在接口函数里面出现了对虚表的访问,通过虚表来调用属于当前类作用域下的Speak函数,从而实现了多态,所以说在构造函数和析构函数中对虚表的赋值就是这个原因,使构造或析构函数中能正确的调用虚表中的函数


析构函数一般声明为虚函数的原因:

在使用父类指针指向子类的时候,进行delete操作,会调用析构函数,如果不是虚析构函数就会调用到父类的析构函数,是虚构函数就会调用子类的虚构函数

对无虚析构函数的delete分析:



对有虚析构函数的delete分析: 



综上所述,虚析构函数被定义为了析构代理函数加入到虚表中,所以析构函数声明为virtual可以使父类指针进行delete时候通过虚表调用到正确的析构函数。



三,多继承篇:

有三个类 Book , Note , BookNote类,class NoteBook:public Book,public Note



首先来看下NoteBook的内存分布:


可以看到内存分布,分别有两个父类,两个虚表,以及自身的成员,父类顺序为声明的继承的顺序。


看下构造函数内存分布:


按照继承的顺序分别调用父类的构造函数,调用前会修正this指针,并且会调用完父类的构造函数后修正虚表指针为本身的虚表地址,在父类的构造函数中又发现:


这说虚表的地址先是父类的虚表地址,这个将使得在父类中通过接口可以正确的调用到属于父类作用域的虚函数。


再来看析构函数:



可以发现析构函数和单继承一样,都先是再次将虚表赋值为自身虚表,只是多继承会将多个虚表赋值,然后修改this指针按照继承相反顺序调用对应类的析构函数。




来看下分别用父类指针指向子类的情景:


1.Book指针指向BookNote类


直接获取第一个虚表(Book)的第二个函数就是Read


再看delete,因为这里是Book的父类指针指向,所以直接调用第一个虚表的第一个虚析构函数。

查看第一个虚表这里的虚析构函数,就是一个构造代理函数的调用



2.Note指针指向BookNote类:


因为是Note指针,所以会将this指针+8来指向Note对象部分,并且调用write是从Note类继承而来,所以会调用第二个虚表的第二个函数 ---> write



这里调用第二个虚表中的第一个函数,跟着看看


发现其实就是将指针-8得到NoteBook的指针,然后调用第一个虚表中的析构代理函数



四,抽象类篇:

纯虚函数:virtual void Fun() = 0;,在类中只是声明不实现,在子类实现,有纯虚函数的类为纯虚类,不能实例化。


看下内存分布和构造函数和其他的继承没什么差别:



但是在父类抽象类中:


这个虚表中存放的是一个__purecall,这个就是在纯虚函数没有进行声明的时候编译器的一个处理:


如果调用了父类未声明的纯虚函数就会报错,所以这个可以作为抽象类的一个判断标志,但是在优化下会优化掉纯虚函数,所以不能说虚表中没有_purecall就不是抽象类。



五,菱形继承篇:

定义四个类,Animal , Horse , Donkey , Mule.

                                            

          

                                                

class Horse : virtual public Animal

class Donkey : virtual public Animal

class Mule : public Horse, public Donkey

通过如上关系继承:

声明一个Mule m;

VS查看这个不方便,直接用OD动态调试看m的内存:


已经分成了四个部分:

先看最后一个部分(粉红色):00428628 -> ,第一个地址为一个函数指针,跟过去看,发现就是Mule的代理析构函数:

而粉红色第二部分值为1,也就是存放Animal对象的地址。

----------------------------------------------------------------------------------------------------------------------------------

再看第一个部分(红色):第一个地址内容为0042860C->,第一个地址值为0,第二个地址值为0x14,this + 0x14刚好指向了Animal对象地址,命名为vHorse_offset

红色部分第二个为2,刚好是Horse成员m_Horse,所以红色部分也就是Horse

----------------------------------------------------------------------------------------------------------------------------------

再看第二个部分(黄色):第一个地址内容为0042861C->,第一个地址值为0,第二个地址值为0xC,    this + 0xC同样刚好指向了Animal对象地址,命名为vDonkey_Offset

黄色第二个值为3,刚好是Donkey成员m_Donkey,所以黄色部分也就是Donkey。

----------------------------------------------------------------------------------------------------------------------------------

再看第三个部分,蓝色部分,只有一个值,就是m_Mule的值的初始化。



分析下构造函数和析构函数的实现:


这个构造函数压入了一个参数 = 1,具体作用跟进Mule构造函数分析:


分析祖父类(Animal)的构造函数:


修过虚表为自身虚表指针,这个虚表指针就是祖父类相关虚函数实现,最后一个成员为0,表示离祖父类偏移为0

Donkey和Horse同级,所以分析一下Donkey的构造函数实现:


同样会先获取到祖父类的虚表,并且修改为当前的类的虚表


析构函数:


析构函数不会和构造函数一样传入参数防止多次调用父类构造函数,而是最后来调用Animal的析构函数。


----------------------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------------------


再分析下分别用父类指针,祖父类指针指向子类是如何实现的:


可以看到了解了内存分布,就手到擒来,就是根据各个父类在Mule中的内存分布获取到偏移相加得到对应的指针。


以上是虚函数只有析构函数的情况,下面看看有除了析构函数其他的虚函数的情况:

---------------------------------------------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------------------------------------------

给Animal加上Eat , Donkey加上TurnMill , Horse加上RunFast的虚函数,则Mule的内存分布如下


1.红色部分:Horse的部分,前四字节为vTable_Horse --> 虚表中有RunFast,中间四个字节为vOffset_Horse , 它地址指向的第二个四字节为此处到Animal的偏移0x1C,第一个字节为-4,猜测应该是用于表示和Horse头部地址的偏移,最后四字节为2,为Horse的成员m_Horse。

2.黄色部分:Donkey的部分,和Horse一样,前四字节为vTable_Donkey --> 虚表中有TurnMill,中间四字节就是到Animal的偏移,最后一个为m_Donkey。

3.蓝色部分为Mule的成员m_Mule。

4.绿色部分暂时未知

5.粉红色部分为Animal部分,前四字节表示vTable_Animal --> 虚表中有Eat函数,以及当前作用域对应的析构函数,最后四字节表示m_Animal成员
















猜你喜欢

转载自blog.csdn.net/a893574301/article/details/80379705
今日推荐