题目
多重继承下的虚函数表与动态类型转换陷阱
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
多重继承虚函数表布局,动态类型转换原理,虚析构函数必要性,对象切片问题,RTTI机制
快速回答
本题考察多重继承场景下的多态行为核心机制:
- 虚函数表在多重继承中的布局:每个基类有独立虚表指针
dynamic_cast通过RTTI验证继承关系,需注意指针偏移计算- 虚析构函数保证完整对象析构链
- 对象切片导致多态行为丢失
- 非虚函数静态绑定与虚函数动态绑定差异
题目代码示例
#include <iostream>
#include <typeinfo>
using namespace std;
class Base1 {
public:
Base1() { cout << "Base1()" << endl; }
virtual ~Base1() { cout << "~Base1()" << endl; }
virtual void foo() { cout << "Base1::foo()" << endl; }
void bar() { cout << "Base1::bar()" << endl; }
};
class Base2 {
public:
Base2() { cout << "Base2()" << endl; }
virtual ~Base2() { cout << "~Base2()" << endl; }
virtual void foo() { cout << "Base2::foo()" << endl; }
void bar() { cout << "Base2::bar()" << endl; }
};
class Derived : public Base1, public Base2 {
public:
Derived() { cout << "Derived()" << endl; }
virtual ~Derived() { cout << "~Derived()" << endl; }
virtual void foo() override {
cout << "Derived::foo()" << endl;
}
void bar() { cout << "Derived::bar()" << endl; }
};
int main() {
Derived d;
Base1* pb1 = &d;
Base2* pb2 = &d;
// 动态类型转换
Base2* pb2_cast = dynamic_cast<Base2*>(pb1);
// 对象切片
Base1 sliced = d;
cout << "=== 虚函数调用 ===" << endl;
pb1->foo(); // (1)
pb2->foo(); // (2)
pb2_cast->foo(); // (3)
cout << "=== 非虚函数调用 ===" << endl;
pb1->bar(); // (4)
pb2->bar(); // (5)
cout << "=== 对象切片调用 ===" << endl;
sliced.foo(); // (6)
sliced.bar(); // (7)
cout << "=== 析构顺序 ===" << endl;
return 0;
}
输出结果分析
Base1()
Base2()
Derived()
=== 虚函数调用 ===
Derived::foo() // (1)
Derived::foo() // (2)
Derived::foo() // (3)
=== 非虚函数调用 ===
Base1::bar() // (4)
Base2::bar() // (5)
=== 对象切片调用 ===
Base1::foo() // (6)
Base1::bar() // (7)
=== 析构顺序 ===
~Derived()
~Base2()
~Base1()
~Base1() // sliced对象析构
核心原理说明
1. 多重继承虚函数表布局
Derived对象内存布局:
| Base1 vptr | Base1数据 | Base2 vptr | Base2数据 | Derived数据 |
↑ ↑ ↑
pb1 this pb2
每个基类有独立的虚表指针:
- Base1虚表:~Base1, Derived::foo()
- Base2虚表:~Base2, Derived::foo()
2. dynamic_cast工作原理
Base2* pb2_cast = dynamic_cast<Base2*>(pb1)过程:
- 通过RTTI获取pb1指向对象的实际类型(Derived)
- 验证Base2是否在Derived的继承链中
- 计算指针偏移:pb1指向Base1子对象,需调整指针到Base2子对象位置
- 返回调整后的有效指针(若转换无效返回nullptr)
3. 虚析构函数必要性
若基类析构函数非虚:
Base1* obj = new Derived();
delete obj; // 仅调用~Base1() → 内存泄漏!
虚析构函数保证通过基类指针删除时调用完整析构链:
~Derived() → ~Base2() → ~Base1()
4. 对象切片问题
Base1 sliced = d;导致:
- 复制构造仅拷贝Base1子对象(Derived部分被截断)
- 虚表指针重置为Base1虚表 → (6)调用Base1::foo()
- 非虚函数(7)静态绑定到Base1::bar()
最佳实践
- 始终声明基类虚析构函数:避免通过基类指针删除时的资源泄漏
- 慎用多重继承:优先使用单一继承+接口继承(纯虚类)
- 避免对象切片:使用指针/引用传递多态对象
- dynamic_cast使用规范:
- 仅用于含虚函数的类(需要RTTI)
- 检查转换结果(指针)或捕获异常(引用)
常见错误
- 基类缺失虚析构函数 → 资源泄漏
- 误用static_cast代替dynamic_cast → 未检查的类型转换风险
- 未识别对象切片 → 多态行为异常
- 在非虚函数中期望多态行为 → 函数静态绑定
- 跨继承分支的dynamic_cast(如菱形继承未虚继承) → 返回nullptr
扩展知识
- 虚继承内存模型:解决菱形继承问题,虚基类共享存储
- RTTI开销:type_info存储于虚表首项,空间换类型安全
- 性能对比:
操作 虚函数调用 dynamic_cast 开销 1次指针跳转 遍历继承链+偏移计算 - C++11 override关键字:显式标记重写,避免隐藏问题