virtual 是 C++ 实现运行时多态的核心机制。默认情况下,通过基类指针调用的函数由静态类型决定(编译期绑定);声明为 virtual 后,调用由动态类型决定(运行时绑定)。
cpp
| |
虚函数表(vtable)与虚函数指针(vptr)
编译器为每个含虚函数的类生成一张虚函数表,存储虚函数地址。每个实例有一个隐藏的 vptr 指向该表:
| |
派生类覆写虚函数时,其 vtable 中对应槽位替换为派生类版本。调用路径:对象 → vptr → vtable → 函数地址。
虚析构函数
基类析构函数必须声明为 virtual,否则通过基类指针 delete 派生类对象时,派生类的析构函数不会被调用,造成资源泄漏:
cpp
| |
经验法则:只要类中有虚函数(说明它被设计为基类),析构函数就应该是虚的。
纯虚函数与抽象类
纯虚函数只声明不实现,语法 = 0。包含纯虚函数的类是抽象类,不能实例化,派生类必须实现所有纯虚函数:
cpp
| |
菱形继承与虚继承
菱形继承(D → B,C → A)会让 D 包含两份 A 的数据,访问时歧义报错:
| |
virtual 继承确保共同祖先只有一份,放在对象内存末尾,通过 vbptr 间接寻址访问。
Q&A
Q1: 请详细解释 C++ 虚函数表的实现原理
包含虚函数的类在编译时生成虚函数表(vtable),每个实例有一个虚函数指针(vptr)指向它。运行时通过 vptr 查到虚函数信息,从而用基类指针也能调到子类重写的版本。
Q2: 多重继承下的虚表布局是怎样的?
多重继承会为每一个基类各存一个 vptr,放在对应基类数据段的前面:
| |
Q3: 虚继承又引入了什么额外开销?
解决的问题: 菱形继承会导致最终类包含两份共同祖先的数据,访问时编译器因歧义报错。虚继承把共同祖先放在内存最后,子类存一个指针(vbptr)指向到基类的偏移,通过间接寻址访问。
代价:
- 内存增长(多了 vbptr 和虚基类表)
- 性能变差(多一次间接寻址)
- 代码更复杂(最终派生类需手动调用祖先构造函数)
实践建议: 优先组合替代继承;需要多重继承时,至少一方应为纯虚接口类。