在 C++ 中,多继承允许一个类从多个基类继承。当涉及虚函数时,多继承可能会引入一些复杂性,特别是在虚函数表(vtable)和虚基类的处理上。
在多继承情况下,每个基类都有自己的虚函数表(vtable)。派生类会继承所有基类的vtable,并可能修改或扩展它们。
多继承下的虚函数表结构:
以下是一个示例,展示了多继承下的虚函数:
#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;
}
Derived 类从 Base1 和 Base2 两个基类继承。Derived 类重写了 Base1 的 show 函数和 Base2 的 display 函数。b1 和 b2 调用虚函数时,实际调用的是 Derived 类中的实现。多继承可能导致复杂的对象内存布局,特别是在涉及虚继承时。
在简单多继承中,派生类对象包含所有基类的子对象。例如,对于上面的 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|
+---------------+
在多继承的情况下,如果使用第二基类的指针通过 new 操作符创建子类对象,需要确保正确删除该对象。为此,基类的析构函数必须是虚函数。
#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;
}
Base1 和 Base2 类的析构函数必须是虚函数,以确保通过基类指针删除对象时,调用的是子类的析构函数。Base2 指针 b2 删除 Derived 对象时,调用了 Derived 类的析构函数,确保对象正确销毁。在 C++ 中,如果基类的析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致派生类中的资源没有被正确释放,从而引发内存泄露。
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;
}
为了避免这种内存泄露问题,基类的析构函数应该声明为虚函数:
Base 类的析构函数声明为虚函数。main 函数中,通过 Base 指针 base 删除 Derived 对象时,会先调用 Derived 的析构函数,正确释放动态分配的内存,然后再调用 Base 的析构函数。class Base {
public:
virtual ~Base() { // 虚析构函数
std::cout << "Base destructor" << std::endl;
}
};
delete 这样的代码没被执行的话)。virtual 也还是虚析构函数,这是 C++ 语言的语法规则),则会触发系统的动态绑定,因为 new 的实际上是一个子类对象,所以先执行的是子类的析构函数,同时编译器还会向子类的析构函数中(具体地说明位置应该在子类析构函数的函数体后面)插入调用父类析构函数的代码,最终实现了先调用子类析构函数,再调用父类析构函数,达到了让整个对象完美释放的目的。override 关键字:明确标记重写的虚函数,避免错误。new 出来的子类对象:基类的析构函数必须是虚函数,以确保正确销毁对象。