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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在深度探索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++中父类含有虚函数时的继承机制及用法的问题。首先,我得回忆一下相关的知识点,并结合提供的引用内容来组织答案。 用户的问题涉及到虚函数继承中的规则和实现细节,特别是虚函数表的处理。根据引用[1],当子类继承父类时,虚函数表会被继承,如果子类重写虚函数虚函数表中的相应条目会被更新为子类的函数地址。而如果子类不重写,虚函数表会保留父类的函数指针,如引用[2]所述。此外,引用[3]提到了析构函数设为虚函数的重要性,避免子类析构不被调用。引用[4]则讨论了多继承虚函数表的排列问题,子类的虚函数表添加在第一个父类的虚表之后。 接下来,我需要将这些信息结构化。首先解释虚函数的基本概念和作用,然后分点讨论继承时的规则,包括虚函数表继承、覆盖、新增虚函数的情况,以及多继承的处理。同时需要提到析构函数设为虚函数的原因,以及不重写虚函数时的行为。 需要注意用户提到的格式要求:行内数学表达式用$...$,独立公式用$$...$$,但这个问题可能不太涉及数学公式,但如果有的话需要正确应用。例如,可以提到虚函数表的地址布局,或者函数指针的存储方式,不过可能不需要复杂的数学表达式。 然后,生成相关问题,这些问题应该围绕用户可能感兴趣的后续内容,比如如何验证虚函数表结构、多继承下的问题、虚析构函数的重要性等。需要确保问题相关且有深度,能够引导用户进一步学习。 最后,检查引用是否正确标注,每个引用标记应与提供的参考资料对应。例如,当讨论子类覆盖虚函数时,引用[1],当提到不重写时引用[2],析构函数相关引用[3],多继承引用[4]。 现在需要确保回答结构清晰,分点说明规则,举例说明,并引用正确。同时,避免使用Markdown格式,但用户给出的示例中使用了代码块,这里可能需要用中文描述代码示例,或者用文字说明,如代码部分用引号标出。 可能还需要指出虚函数表的创建和更新是由编译器自动处理的,子类不会复制父类的虚表,而是继承后修改,这一点在引用[1]中有详细说明。 总结回答的结构: 1. 虚函数的基本概念和作用 2. 继承规则: a. 虚函数表继承 b. 覆盖虚函数时的更新 c. 新增虚函数 d. 多继承情况下的处理 3. 析构函数设为虚函数的重要性 4. 不重写虚函数的结果 5. 代码示例说明 6. 相关问题 需要确保每个部分都有对应的引用支持,并且语言简洁准确。例如,在讨论多继承时引用[4],说明虚函数表的位置安排。 现在开始组织这些内容,确保符合用户给出的格式要求,分点清晰,使用中文,并正确添加引用标记。</think>### C++父类含有虚函数时的继承机制及实现细节 在C++中,**虚函数**是实现运行时多态的核心机制。当父类包含虚函数时,子类继承的规则和实现细节如下: --- #### 1. **虚函数表(vtable)的继承与覆盖** - **虚函数表基础**:每个包含虚函数的类会生成一个虚函数表(存储指向虚函数的指针)。例如,父类`Base`的虚函数表包含其虚函数地址[^1]。 - **子类继承规则**: - 若子类**不重写父类虚函数**,则直接继承父类虚函数表中的函数地址。例如,子类`Derived`未重写时,调用虚函数会执行父类版本[^2]。 - 若子类**重写虚函数**,编译器会更新虚函数表中对应的条目,替换为子类函数的地址[^1]。 - **示例**: ```cpp class Base { public: virtual void func() { cout << "Base"; } }; class Derived : public Base { public: void func() override { cout << "Derived"; } // 虚函数表条目被更新 }; ``` --- #### 2. **新增虚函数虚函数表扩展** - 子类可以**新增虚函数**,此时虚函数表会在继承的父类虚表后追加新条目。 - **示例**: ```cpp class Derived : public Base { public: virtual void newFunc() {} // 新增虚函数虚函数表扩展 }; ``` --- #### 3. **多继承下的虚函数表** - 在多继承中,子类的虚函数表会按继承顺序排列。例如,继承`Base1`和`Base2`时,子类虚函数表会先包含`Base1`的虚表,再添加`Base2`的虚表,最后追加子类新增的虚函数[^4]。 - **覆盖不同父类的虚函数**时,每个父类虚表中对应的条目会被独立更新。 --- #### 4. **虚析构函数的重要性** - **规则**:若父类析构函数未声明为虚函数,当通过父类指针删除子类对象时,**子类析构函数不会被调用**,导致内存泄漏[^3]。 - **正确用法**: ```cpp class Base { public: virtual ~Base() {} // 必须声明为虚函数 }; ``` --- #### 5. **不重写虚函数的行为** - 若子类不重写父类虚函数,调用时直接使用父类实现。例如: ```cpp Derived d; Base* ptr = &d; ptr->func(); // 输出"Base"[^2] ``` --- #### 6. **验证虚函数表结构(调试技巧)** - 通过调试器内存窗口查看对象内存布局虚函数表指针通常位于对象起始位置。 - 多继承下需注意虚函数表的合并与扩展问题[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值