• C++ 函数语义学——多继承虚函数深释、第二基类与虚析构必加


    多继承与虚函数:深入理解与最佳实践

    1. 多继承下的虚函数机制

    在 C++ 中,多继承允许一个类从多个基类继承。当涉及虚函数时,多继承可能会引入一些复杂性,特别是在虚函数表(vtable)和虚基类的处理上。

    1.1 虚函数表的结构

    在多继承情况下,每个基类都有自己的虚函数表(vtable)。派生类会继承所有基类的vtable,并可能修改或扩展它们。
    多继承下的虚函数表结构:

    1. 派生类对象通常包含多个vptr(虚函数表指针),每个对应一个基类。
    2. 每个vptr指向一个独立的vtable。
    3. 派生类重写的虚函数会更新相应的vtable条目。
    1.2 示例代码

    以下是一个示例,展示了多继承下的虚函数:

    #include 
    
    class Base1 {
    public:
        virtual void show() {
            std::cout << "Base1::show" << std::endl;
        }
    };
    
    class Base2 {
    public:
        virtual void display() {
            std::cout << "Base2::display" << std::endl;
        }
    };
    
    class Derived : public Base1, public Base2 {
    public:
        void show() override {
            std::cout << "Derived::show" << std::endl;
        }
    
        void display() override {
            std::cout << "Derived::display" << std::endl;
        }
    };
    
    int main() {
        Derived derived;
        Base1* b1 = &derived;
        Base2* b2 = &derived;
    
        b1->show();     // 调用 Derived::show
        b2->display();  // 调用 Derived::display
    
        return 0;
    }
    
    1.3 解释
    • 多继承Derived 类从 Base1Base2 两个基类继承。
    • 虚函数重写Derived 类重写了 Base1show 函数和 Base2display 函数。
    • 多态性:通过基类指针 b1b2 调用虚函数时,实际调用的是 Derived 类中的实现。

    2. 多继承中的对象布局

    多继承可能导致复杂的对象内存布局,特别是在涉及虚继承时。

    2.1 简单多继承的对象布局

    在简单多继承中,派生类对象包含所有基类的子对象。例如,对于上面的 Derived 类,其内存布局可能如下:

    Derived对象:
    +---------------+
    | Base1's vptr  | --> Base1's vtable: [Derived::show]
    +---------------+
    | Base1's data  |
    +---------------+
    | Base2's vptr  | --> Base2's vtable: [Derived::display]
    +---------------+
    | Base2's data  |
    +---------------+
    | Derived's data|
    +---------------+
    

    3. 如何删除用对第二基类指针 new 出来的子类对象

    在多继承的情况下,如果使用第二基类的指针通过 new 操作符创建子类对象,需要确保正确删除该对象。为此,基类的析构函数必须是虚函数。

    3.1 示例代码
    #include 
    
    class Base1 {
    public:
        virtual ~Base1() {
            std::cout << "Base1 destructor" << std::endl;
        }
    };
    
    class Base2 {
    public:
        virtual ~Base2() {
            std::cout << "Base2 destructor" << std::endl;
        }
    };
    
    class Derived : public Base1, public Base2 {
    public:
        ~Derived() {
            std::cout << "Derived destructor" << std::endl;
        }
    };
    
    int main() {
        Base2* b2 = new Derived();
        delete b2;  // 调用 Derived 的析构函数
    
        return 0;
    }
    
    3.2 解释
    • 虚析构函数Base1Base2 类的析构函数必须是虚函数,以确保通过基类指针删除对象时,调用的是子类的析构函数。
    • 正确删除对象:通过 Base2 指针 b2 删除 Derived 对象时,调用了 Derived 类的析构函数,确保对象正确销毁。

    4. 父类非虚析构函数时导致的内存泄露演示

    在 C++ 中,如果基类的析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致派生类中的资源没有被正确释放,从而引发内存泄露。

    4.1 示例代码(内存泄露情况)
    • 非虚析构函数Base 类的析构函数不是虚函数。
    • 动态分配内存Derived 类在构造函数中动态分配了一块内存,并在析构函数中释放这块内存。
    • 内存泄露:在 main 函数中,通过 Base 指针 base 删除 Derived 对象时,只调用了 Base 的析构函数,没有调用 Derived 的析构函数,导致 Derived 类中动态分配的内存没有被释放,从而引发内存泄露。
    #include 
    
    class Base {
    public:
        ~Base() {  // 非虚析构函数
            std::cout << "Base destructor" << std::endl;
        }
    };
    
    class Derived : public Base {
    public:
        Derived() {
            data = new int[100];  // 动态分配内存
        }
    
        ~Derived() {
            delete[] data;  // 释放内存
            std::cout << "Derived destructor" << std::endl;
        }
    
    private:
        int* data;
    };
    
    int main() {
        Base* base = new Derived();
        delete base;  // 只调用 Base 的析构函数,不调用 Derived 的析构函数
    
        return 0;
    }
    
    4.2 解决方法

    为了避免这种内存泄露问题,基类的析构函数应该声明为虚函数:

    • 虚析构函数Base 类的析构函数声明为虚函数。
    • 正确释放内存:在 main 函数中,通过 Base 指针 base 删除 Derived 对象时,会先调用 Derived 的析构函数,正确释放动态分配的内存,然后再调用 Base 的析构函数。
    class Base {
    public:
        virtual ~Base() {  // 虚析构函数
            std::cout << "Base destructor" << std::endl;
        }
    };
    
    4.3 为什么父类用了虚析构函数后,执行结果就正常了?
    • 非虚析构函数:不会触发动态绑定,只调用父类析构函数。
      • 如果父类的析构函数不是虚函数,则不会触发动态绑定,结果就是只会调用父类的析构函数而不会调用子类的析构函数,从而可能导致内存泄露(如果子类的析构函数中存在诸如 delete 这样的代码没被执行的话)。
    • 虚析构函数:触发动态绑定,先调用子类析构函数,再调用父类析构函数。
      • 如果父类的析构函数是虚函数,则子类的析构函数一定是虚函数(就算子类析构函数前面不加 virtual 也还是虚析构函数,这是 C++ 语言的语法规则),则会触发系统的动态绑定,因为 new 的实际上是一个子类对象,所以先执行的是子类的析构函数,同时编译器还会向子类的析构函数中(具体地说明位置应该在子类析构函数的函数体后面)插入调用父类析构函数的代码,最终实现了先调用子类析构函数,再调用父类析构函数,达到了让整个对象完美释放的目的。

    5. 多继承的最佳实践

    1. 慎用多继承:多继承可能导致设计复杂化,应谨慎使用。
    2. 接口继承vs实现继承:优先使用接口继承(纯虚函数)而非实现继承。
    3. 避免菱形继承:如果无法避免,使用虚继承。
    4. 正确处理析构函数:基类析构函数应为虚函数。
    5. 使用 override 关键字:明确标记重写的虚函数,避免错误。

    总结

    • 多继承下的虚函数:子类可以重写多个基类的虚函数,通过基类指针调用虚函数时,会根据对象的动态类型调用子类的实现。
    • 对象布局:多继承会导致复杂的对象内存布局,包含多个vtable指针。
    • 删除用第二基类指针 new 出来的子类对象:基类的析构函数必须是虚函数,以确保正确销毁对象。
    • 非虚析构函数导致内存泄露:基类的析构函数应该声明为虚函数,以确保通过基类指针删除对象时,调用的是派生类的析构函数,正确释放资源。
    • 最佳实践:慎用多继承,正确处理虚函数和析构函数,遵循接口继承原则。
  • 相关阅读:
    Dynamsoft BarcodeReader SDK Java 9.6.30 Crack
    秋季开学,培训机构如何做好线下招生?
    上周热点回顾(2.28-3.6)
    css实现流星划过动画
    前端npm详解
    netty系列之:netty中的核心编码器bytes数组
    图论学习笔记 - 二分图的匹配
    2.4 设计多线程的程序
    14:00面试,14:05就出来了,问的问题有点变态。。。
    【Linux】关于进程
  • 原文地址:https://blog.csdn.net/qq_68194402/article/details/141108562