验证C++多继承下的虚函数表的布局

本文深入探讨了C++中多继承情况下虚函数表的构建方式,以实例介绍了如何处理虚析构函数、继承的虚函数及类型不同的克隆函数等问题,并通过代码验证了这些概念。

在深度探索C++对象模型一书第四章中,探讨了虚函数表的构建方式,例如以下定义的三个类:

class Base1 {
public:
    Base1() {}
    virtual ~Base1() { cout << "Base1::~Base1" << endl; }
    virtual void speakClearly() { cout << "Base1::speakClearly" << endl; }
    virtual Base1 *clone() const { cout << "Base1::clone" << endl; return new Base1(); }
protected:
    float data_Base1;
};

class Base2 {
public:
    Base2() {}
    virtual ~Base2() { cout << "Base2::~Base2" << endl; }
    virtual void mumble() { cout << "Base2::mumble" << endl; }
    virtual Base2 *clone() const { cout << "Base2::clone" << endl; return new Base2();
    }
protected:
    float data_Base2;
};

class Derived : public Base1, public Base2 {
public:
    Derived() {}
    virtual ~Derived() { cout << "Derived::~Derived" << endl; }
    virtual Derived *clone() const { cout << "Derived::clone" << endl; return new Derived(); }
    virtual void thinkCarefully() { cout << "Derived::thinkCarefully" << endl; }
protected:
    float data_Derived;
};


那么派生类的内存布局如图所示:


其中编译器实现的难点在于图中加星号的几个函数,三个要解决的问题是(1)virtual destructor,(2)被继承下来的Base::mumble(),(3)一组clone函数实体。这几个问题的产生都是缘于多继承中除第一个父类之外的其他父类的指针,在指向一个派生类对象时,都需要位移一段offset,例如以下的代码:

Base1 pb1 = new Derived();
Base2 pb2 = new Derived();
Derived pd = new Derived();

参考上面图片的第三个模型,其中pb1和pd均是指向对象的起始地址,而pb2则指向第二个子对象,如果不这么做,那么连最基本的非多态调用,例如访问数据 pbase->data_Base2  这样的操作都无法支持,因为pb2指向哪个对象到运行期才能知道,在编译期只能按照Base2的内存布局通过位移来访问数据成员。那么问题就清晰了:(1)通过 delete pb2 调用析构函数时,要析构的是整个派生类的对象,指针必须调整到pd的位置;(2)第二个问题则相反,当执行派生类对象指针调用从第二个父类继承下来的虚函数,例如执行 pd->mumble() 时pd需要调整位置到pb2处;(3)第三个问题与C++支持的一个语言特性有关,子类函数overwrite父类虚函数时,允许返回值类型有所不同,可以是一个publicly derived type,上述代码如果执行 pb2->clone() 创建的是Derived类型的对象,因此也会涉及到指针调整的问题。


按照书中所述,编译器实现的方式可能有更改虚表结构,引入thunk技术、“address points”策略等等,由于比较复杂并没有展开描述。总之,虚表的某些slot所指向的函数可能会发生变化,如同上图中带*的几个函数。写了一段代码来验证一下,也就是通过地址转换来访问虚表slot所指向的各个函数(一个地址可以用unsigned int型整数来表示)。为了加深理解,我在Derived里面添加了一个新函数 virtual void thinkCarefully(),事实证明它会跟在 Derived::clone() 之后,也就是说应该在 Base2::mumble() 之前。编译器为VC++2015。

typedef void (*Fun)(void);
typedef unsigned int UINT;

int main(void)
{
    Fun fun, fun1, fun2, fun3, fun4;

    {
        Base1 b1;
        cout << "--------------------------------------------------" << endl;
        cout << "Address of Base1 vtable:" << (UINT *)(&b1) << endl;
        cout << "Address of 1st function in Base1 vtable:" << (UINT *) *(UINT *)(&b1) << endl;

        fun = (Fun)(*(UINT *) *(UINT *)(&b1));
        //fun();
        fun1 = (Fun)(*((UINT *) *(UINT *)(&b1) + 1));
        fun1();
        fun2 = (Fun)(*((UINT *) *(UINT *)(&b1) + 2));
        fun2();

        Base2 b2;
        cout << "--------------------------------------------------" << endl;
        cout << "Address of Base2 vtable:" << (UINT *)(&b2) << endl;
        cout << "Address of 1st function in Base2 vtable:" << (UINT *) *(UINT *)(&b2) << endl;

        fun = (Fun)(*(UINT *) *(UINT *)(&b2));
        //fun();
        fun1 = (Fun)(*((UINT *) *(UINT *)(&b2) + 1));
        fun1();
        fun2 = (Fun)(*((UINT *) *(UINT *)(&b2) + 2));
        fun2();

        Derived d;
        cout << "--------------------------------------------------" << endl;
        cout << "Address of Derived vtable:" << (UINT *)(&d) << endl;
        cout << "Address of 1st function in Derived vtable:" << (UINT *) *(UINT *)(&d) << endl;

        fun = (Fun)(*(UINT *) *(UINT *)(&d));
        //fun();
        fun1 = (Fun)(*((UINT *) *(UINT *)(&d) + 1));
        fun1();
        fun2 = (Fun)(*((UINT *) *(UINT *)(&d) + 2));
        fun2();
        fun3 = (Fun)(*((UINT *) *(UINT *)(&d) + 3));
        fun3();
        fun4 = (Fun)(*((UINT *) *(UINT *)(&d) + 4));
        //fun4();

        cout << "--------------------------------------------------" << endl;
        cout << "Address of 2nd Derived vtable:" << (UINT *)(&d) + 1 << endl;
        cout << "Address of 1st function in 2nd Derived vtable:" << (UINT *) *((UINT *)(&d) + 2) << endl;

        fun = (Fun)(*(UINT *) *((UINT *)(&d) + 2));
        //fun();
        fun1 = (Fun)(*((UINT *) *((UINT *)(&d) + 2) + 1));
        fun1();
        fun2 = (Fun)(*((UINT *) *((UINT *)(&d) + 2) + 2));
        fun2();

        cout << "--------------------------------------------------" << endl;
    }

    std::cin.get();
    return 0;
}

其中fun函数被注释掉,因为他们代表的是析构函数,为了让程序运行完当然不能执行这些函数。被注释掉的fun4对应的是第一个虚表的Base2::mumble,这里需要调整到第二个虚表的Base2::mumble,因此不是一个“简单”的函数,有可能牵扯到thunk,也有可能这里根本就是一个无效的地址(编辑器采用了其他实现方式),因此也无法在这里调用函数。令我不解的是fun2函数,它对应的是clone函数,按理说不可能所有的clone都是“单纯”的函数,但实际上将这些地址转化为 void (*)(void) 之后都成功执行了,只能理解为VC++编译器为解决这种问题做了些特殊处理。下图是程序的执行情况,包括最后栈空间内的三个对象被析构所产生的输出。


<think>好的,我现在需要回答用户关于C++多继承情况下虚函数表的结构和工作原理的问题。首先,我得回忆一下自己对虚函数表(vtable)和多继承的理解,然后结合提供的引用资料来整理出一个清晰的解释。 用户的问题主要聚焦在多继承虚函数表布局和机制。根据已有的知识,单继承的情况下,每个类有一个虚函数表,派生类会继承基类的虚表,并替换自己重写的函数指针。但多继承时情况更复杂,因为一个派生类可能继承多个基类,每个基类都有自己的虚表。 根据引用[4]中的示例,Derived类同时继承Base1和Base2,每个基类都有自己的虚函数show和display。当Derived重写这两个函数时,它需要为每个基类维护各自的虚表。这意味着在Derived对象的内存布局中,会有两个虚表指针,分别指向Base1和Base2的虚表。每个虚表中对应的函数被替换为Derived的实现。这可能涉及到虚表指针的调整,尤其是在处理多个基类时,可能存在不同的偏移量。 引用[2]提到虚继承用于解决菱形继承问题,但用户的问题是关于多继承,不是虚继承。不过其中提到的虚基类指针和表可能相关,但需要区分。多继承中的虚函数表结构主要涉及每个基类独立的虚表,而虚继承更多处理共享基类的问题。 引用[5]中的代码示例展示了如何通过指针访问虚表中的函数,即使它们是私有的。这说明每个对象实例包含指向虚表的指针,而多继承时可能有多个这样的指针。例如,Derived对象会有两个vptr,分别对应Base1和Base2的虚表。每个虚表中存储的是该基类对应的虚函数地址,派生类重写的函数会覆盖基类中的对应项。 此外,引用[3]提到多重继承与虚函数表的结构,可能涉及派生类自己的虚函数和多个基类虚表的合并。例如,如果派生类新增了虚函数,可能会附加到第一个基类的虚表中,或者单独处理,具体取决于编译器实现。不过通常,每个基类对应一个虚表,派生类新增的虚函数可能放在第一个虚表的末尾,或者有其他的处理方式。 需要解释的是,当通过不同的基类指针调用虚函数时,编译器会根据对象的虚表指针找到对应的函数地址。例如,在引用[4]的示例中,Base1*调用show()会使用Base1的虚表,而Base2*调用display()使用Base2的虚表,两者都指向Derived的重写版本。 可能还需要讨论对象的内存布局,比如每个基类的子对象在派生类中的排列顺序,以及虚表指针的位置。例如,Derived对象可能先包含Base1的子对象(包括其vptr),然后是Base2的子对象(包括另一个vptr),最后是Derived自己的成员变量。每个基类的虚表指针指向各自的虚表,其中虚函数被替换为派生类的实现。 同时,引用[1]提到虚函数继承是接口继承,派生类需要重写以实现多态。所以在多继承中,每个基类的虚函数接口都需要被正确重写,并在各自的虚表中更新。 总结一下,多继承虚函数表布局是每个基类对应一个虚表指针,派生类为每个基类维护各自的虚表,替换重写的函数,保持未重写的函数指向基类实现。对象内存中包含多个vptr,每个指向对应的基类虚表。调用虚函数时,根据指针类型找到对应的vptr,进而找到函数地址。</think>### C++多继承虚函数表布局机制 #### 一、多继承虚函数表核心结构 1. **每个基类独立虚表指针** 多继承情况下,派生类会为**每个含有虚函数的基类**维护独立的虚函数表指针(vptr)。例如派生类$Derived$继承$Base1$和$Base2$时,对象内存布局如下: ``` | Base1::vptr | Base1成员 | Base2::vptr | Base2成员 | Derived成员 | ``` 每个vptr指向对应基类的虚函数表[^4][^5]。 2. **虚表内容组合规则** - 基类虚表中**未被重写**的虚函数保留原地址 - **被重写**的虚函数替换为派生类实现地址 - 若派生类**新增虚函数**,通常附加到**第一个基类虚表**末尾[^3][^5] #### 二、虚函数调用机制 ```cpp Base1* b1 = new Derived(); Base2* b2 = new Derived(); b1->show(); // 通过Base1的vptr查找虚表 b2->display(); // 通过Base2的vptr查找虚表 ``` 编译器会根据指针类型: 1. 定位到对应基类子对象的vptr 2. 通过虚表偏移量找到函数地址 3. 自动插入`this指针调整`代码(当基类子对象存在偏移时)[^2][^4] #### 三、特殊场景处理 1. **菱形继承问题** 使用虚继承时,共享基类的虚函数会通过**虚基类表(vbtable)** 单独维护,与普通虚表分离[^2]: ```cpp class A { virtual void f(); }; class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {}; ``` 此时$D$对象包含: ``` | B::vptr | C::vptr | A::vptr | B成员 | C成员 | D成员 | ``` 2. **动态类型转换** `dynamic_cast`依赖虚表中的**RTTI信息**,通过遍历继承树实现类型安全检查[^3]。 #### 四、验证示例(基于引用[5]扩展) ```cpp #include <iostream> using namespace std; class Base1 { public: virtual void f1() { cout << "Base1::f1\n"; } }; class Base2 { public: virtual void f2() { cout << "Base2::f2\n"; } }; class Derived : public Base1, public Base2 { public: void f1() override { cout << "Derived::f1\n"; } void f2() override { cout << "Derived::f2\n"; } virtual void f3() { cout << "Derived::f3\n"; } }; int main() { Derived d; Base1* b1 = &d; Base2* b2 = &d; // 验证虚表布局 cout << "Base1虚表地址:" << *(void**)b1 << endl; cout << "Base2虚表地址:" << *(void**)b2 << endl; // 调用验证 b1->f1(); // Derived::f1 b2->f2(); // Derived::f2 } ``` 输出结果: ``` Base1虚表地址:0x401560 Base2虚表地址:0x401578 Derived::f1 Derived::f2 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值