面向对象(三):多态与虚函数
原理
虚函数表(vtable)的结构
每个包含虚函数的类(或从虚基类派生的类)有一张虚函数表。vtable 是一个函数指针数组,存储在只读数据段(.rodata)。该类的所有对象共享同一张 vtable。
Base 对象 Base vtable
┌──────────┐ ┌──────────────────┐
│ vptr ────┼───────────→│ &Base::func1 │ ← slot 0
├──────────┤ │ &Base::func2 │ ← slot 1
│ baseData │ │ &type_info(Base) │ ← RTTI
└──────────┘ └──────────────────┘
Derived 对象 Derived vtable
┌──────────┐ ┌──────────────────┐
│ vptr ────┼───────────→│ &Derived::func1 │ ← slot 0 (重写)
├──────────┤ │ &Base::func2 │ ← slot 1 (未重写)
│ baseData │ │ &Derived::func3 │ ← slot 2 (新增)
├──────────┤ │ &type_info(Derived)│ ← RTTI
│derivedData│ └──────────────────┘
└──────────┘
关键事实:
- vptr 位于对象内存的起始位置(偏移 0)
- 每个类只有一张 vtable,所有对象共享
- 非虚函数不在 vtable 中,调用时静态绑定
- vptr 在构造函数执行时初始化——先初始化为基类 vtable,再切换为派生类 vtable
虚函数调用的汇编路径
; ptr->func1() — 虚函数调用需要两次内存读
mov rax, [rdi] ; rax = vptr(读 this 前 8 字节)
mov rax, [rax] ; rax = vtable[0](读 vtable 第一项)
call rax ; 间接跳转到实际函数
; ptr->nonVirtual() — 非虚函数只需一次直接调用
call Base::nonVirtual虚函数调用的开销:额外一次指针解引用 + 无法内联优化。对于热路径中的紧密循环,这个开销可能显著;对于大多数场景可忽略不计。
构造/析构期间的 vptr 行为
构造函数中调用的虚函数使用的是当前类的版本,不是最终派生类的版本:
class Base {
Base() { virtualFunc(); } // 调用的是 Base::virtualFunc
virtual void virtualFunc() {}
};
class Derived : public Base {
Derived() { virtualFunc(); } // 调用的是 Derived::virtualFunc
void virtualFunc() override {}
};原因:执行 Base 构造函数时,vptr 指向 Base 的 vtable。Base 构造完成后进入 Derived 构造时,vptr 才更新为 Derived 的 vtable。析构过程相反——先切换到 Derived 的 vtable,执行完派生类析构后再切回 Base 的 vtable。
RTTI 与 type_info
vtable 中还存储 type_info 对象,为 dynamic_cast 和 typeid 提供支撑。dynamic_cast 通过查询 vtable 中的 RTTI 信息确定运行时类型,实现安全的向下转型。
; dynamic_cast<Derived*>(basePtr) 的简化流程
; 1. 读取 vptr → 获取 vtable 地址
; 2. 从 vtable 中读取 type_info
; 3. 遍历继承层级比较类型信息
; 4. 返回调整后的指针或 nullptr禁用 RTTI(-fno-rtti)后 dynamic_cast 和 typeid 不可用。
虚函数默认参数的陷阱
默认参数是静态绑定的——在编译期根据指针/引用的静态类型决定。虚函数本身是动态绑定的:
Base* ptr = new Derived();
ptr->print(10); // 使用 Base::print 的默认参数(10),但调用 Derived::print
// 结论:永远不要在重写的虚函数中改变默认参数值语法
virtual 与 override
class Base {
public:
virtual void draw() const { /* ... */ } // 声明虚函数
virtual std::string name() const = 0; // 纯虚函数
virtual ~Base() = default; // 虚析构(必须)
};
class Derived : public Base {
public:
void draw() const override { /* ... */ } // 重写并使用 override 检查
std::string name() const override { return "Derived"; }
};纯虚函数与抽象类
class Shape { // 抽象类——不能实例化
public:
virtual double area() const = 0; // 纯虚函数
virtual ~Shape() = default;
};纯虚函数可以有实现(virtual void f() = 0 {}),派生类可选择调用默认实现。
final 阻止重写(C++11)
class Base {
virtual void critical() final { /* 不允许被重写 */ }
};协变返回类型
class Base {
virtual Base* clone() const { return new Base(*this); }
};
class Derived : public Base {
Derived* clone() const override { return new Derived(*this); }
// 返回类型从 Base* 协变为 Derived*
};dynamic_cast 与 typeid
// 对指针:失败返回 nullptr
if (auto* d = dynamic_cast<Derived*>(basePtr)) { d->derivedOnly(); }
// 对引用:失败抛 std::bad_cast
Derived& d = dynamic_cast<Derived&>(*basePtr);
// typeid
if (typeid(*basePtr) == typeid(Derived)) { /* 精确类型判断 */ }实践
力扣题目:力扣数组题(可设计 Student 基类 + 不同评价标准的派生类),力扣排序(抽象比较逻辑)。
AI 自检:1) 给 AI 一个包含虚函数的类层次,要求 AI 画出完整的 vtable 布局;2) 问”为什么构造/析构函数中不应该调用虚函数”——要求 AI 从 vptr 切换时间线来说明;3) 给虚函数添加默认参数的危险代码,问 AI 为什么输出不符合预期。
class A {
public:
virtual void f(int x = 10) { cout << "A: " << x << endl; }
};
class B : public A {
public:
void f(int x = 20) override { cout << "B: " << x << endl; }
};
A* p = new B(); p->f(); // 输出?为什么?建议先阅读:05_面向对象(一)类与对象基础 — vptr 在对象布局中的位置;06_面向对象(二)继承 — 派生类对象的完整内存结构;ASM_03_C++在汇编层的体现 — 虚调用在汇编层的完整路径。