深入理解C++面向对象机制(一)多继承

深入理解C++面向对象机制(一)多继承


零.声明


1.《深入理解C++面向对象机制》系列的博文是博主阅读《深度探索C++对象模型》之后的自我总结性质的文章。当然也希望这些文章能够帮助那些想深入了解C++的网友。

2.文章中会有一些被称为“编译器生成的代码”,这些代码并不是编译器真正的生成代码,只是为了方便讨论而写的模拟代码。

3.如果觉得文章对你有帮助而需要转载,也请阁下能够注明出处。

4.如果觉得博文对问题的讨论有误,也可以给博主留言。

 

一.引入


我们在《深入理解C++面向对象机制(零)单继承》中讨论了C++的面向对象的一个核心特性多态。在单继承下的环境下,我们理解了C++是如何实现虚函数的。本文将继续讨论,请情况扩充到多继承。多继承相对于单继承,多了一项工作,this指针的调整。

 

二.多继承下的虚函数


1. this调整


我们先来看一个普通的多继承情况,我们为什么要调整this指针。

class CDerived : public CBase1, CBase2
CBase2 * p = new CDerived;

编译器对多继承类的存放方式就如下图。

 

图1.0

 

类CDerived的对象中先存放是第一个基类CBase1的subobject的数据,然后接着的是第二个基类的subobject。最后才是CDerived新定义的数据。

所以上面的两行代码在编译器看来可能会变成这样:

CDerived * pTemp = __new(sizeof(CDerived));   //1
pTemp = CDerived::CDerived(pTemp);            //2
CBase2 * p = pTemp ? pTemp + sizeof(CBase1) : 0;//3
//析构函数会在__delete之前调用,但是这里先不做讨论
__delete(p ? p - sizeof(CBase1) : 0);        //4

第一行和第二行代码,就是编译器为CDerived对象分配内存并调用构造函数;

第三行代码,pTemp是指向CDerived对象的开始处,但是CBase2 *p指针需要调整pTemp,移动sizeof(CBase1)才能到CBase2的subobject。这里就是在this指针的调整。

第四行代码,delete对象,我们需要将指针p移回CDerived的开始处。

假如在CDerived对象调用它的虚函数,需要传入this指针的时候,也是要考虑this指针的调整。

 

2.Thunk技术

class CBase1
{
public:
    CBase1();
    virtual ~CBase1();
private:
    int m_x;
 
public:
    virtual void Fun();
    virtual void Fun1_1();
    virtual void Fun1_2();
    void Fun1_3();
};
 
class CBase2
{
public:
    CBase1();
    virtual ~CBase1();
private:
    int m_y;
public:
    virtual void Fun();
    virtual void Fun2_1();
    virtual void Fun2_2();
    void Fun2_3();
};
 
class CDerived : public CBase1, CBase2
{
public:
    CDerived();
    ~CDerived();
private:
    int m_z;
public:
    virtual void Fun();
    virtual void Fun1_1();
    virtual void Fun2_1();
};

按照之前的讨论方式,接下来就是这几个类的virtual table图。


图1.1

 

图1.1中展示了两个基类的virtualtable图。

现在我们介绍Thunk技术。从图1.1可以看出,CDerived类的第一个virtual table,明显比第二个大。而第二个virtual table其中的几个函数就是CBase2的virtual table那几个函数,只是其中在几个函数(析构函数、Fun和Fun2_1)被替换成了CDerived中定义的函数。

再来看一个表,里面不光包含了CBase1的几个函数也包括了CBase2的函数。

从这两个表就可以大致看出Thunk的做法,第一个表作为主表,而后的表作为次表。主表包含CDerived所要用到的所有虚函数的地址,次表则包括第二个基类所要用到的虚函数地址(如果有第三个基类,就会第二张次表)。

接下来就要看一下,不管主表还是次表都有一个槽是被涂成了深灰色。这几个槽中的虚函数,就需要调整this指针的。

 

3.其他的技术


除了上面的Thunk技术,还有一种简单明了的办法。

在virtual table中存放一个虚函数指针(pFun),还存放一个this指针的调整值(nOffset)。

比如下面代码:

CBase2 * p = new CDerived;

p->Fun();

编译器会将第二行代码解释成下面这样:

(*p->vptr[2].pFun)(p+ p->vptr[2].nOffset);

这种方法比较好理解。但是这样有一个坏处,就是每一个虚函数槽都要放一个offset,即使那些不需要this指针调整。这就造成了不必要的浪费。

 

三.结尾


现在我们了解了多继承下的虚函数实现方式。单继承延生至多继承,就是如何高效地解决this指针调整的问题。接下来将会讨论虚拟继承(一种比较特殊的继承方式)。

 

 

 

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值