面向对象(三):多态与虚函数

原理

虚函数表(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

vtable 的内存布局详解

graph TD
    subgraph Derived_vtable["Derived vtable (.rodata)"]
        V0["slot 0: &Derived::func1"]
        V1["slot 1: &Base::func2"]
        V2["slot 2: &Derived::func3"]
        VRTTI["RTTI: type_info(Derived)"]
        V00["slot -1: offset_to_top"]
        V01["slot -2: &type_info(Derived)"]
    end

    subgraph Derived_obj["Derived object (stack/heap)"]
        VPTR["vptr ──────→"]
    end

    VPTR --> V00
    VPRTI2["vptr 实际指向 offset_to_top 上方"] -.-> VPTR

    style V0 fill:#4CAF50,color:#fff
    style V1 fill:#FF9800,color:#fff
    style V2 fill:#4CAF50,color:#fff

实际 vtable 的布局比上图更复杂。在 Itanium ABI(Linux/macOS 使用的 ABI)下,vtable 包含:

vtable 内存布局(从低地址到高地址):
┌─────────────────────────────────┐
│ offset_to_top = 0               │  ← 虚基类偏移
│ &type_info(Derived)             │  ← RTTI 信息
├─────────────────────────────────┤
│ &Derived::func1                 │  ← slot 0(offset 0)
│ &Base::func2                    │  ← slot 1(offset 8)
│ &Derived::func3                 │  ← slot 2(offset 16)
└─────────────────────────────────┘
vptr 指向 slot 0(不指向 type_info)

虚函数调用的汇编路径

; 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。

// vptr 切换时间线
Widget w; // 构造过程:
// 1. 分配内存
// 2. Base::Base() → vptr = &Base::vtable
//    调用 Base 中的虚函数 → 调用 Base 版本
// 3. Derived::Derived() → vptr = &Derived::vtable
//    调用 Derived 中的虚函数 → 调用 Derived 版本
// 4. 构造完成,vptr 稳定指向 Derived::vtable
 
// 析构过程:
// 1. ~Derived() → vptr = &Derived::vtable(已经是)
//    执行 Derived 析构体
// 2. ~Base() → vptr = &Base::vtable(切回基类版本)
//    调用虚函数 → 调用 Base 版本(如果此时调用虚函数)
// 3. 释放内存

RTTI 与 type_info 内部实现

vtable 中还存储 type_info 对象,为 dynamic_casttypeid 提供支撑。dynamic_cast 通过查询 vtable 中的 RTTI 信息确定运行时类型,实现安全的向下转型。

; dynamic_cast<Derived*>(basePtr) 的简化流程
; 1. 读取 vptr → 获取 vtable 地址
; 2. 从 vtable 中读取 type_info
; 3. 遍历继承层级比较类型信息
; 4. 返回调整后的指针或 nullptr
// type_info 的典型实现结构
struct type_info {
    const char* __type_name; // 类型名称字符串
    // 内部比较逻辑:通过 __type_name 指针地址比较
    // 相同类型共享同一个 type_info 对象
};
 
// dynamic_cast 的实现流程(简化):
// 1. 获取源对象的 type_info(从 vtable)
// 2. 获取目标类型的 type_info(编译期已知)
// 3. 在继承图中查找:源类型是否在目标类型的继承链上?
// 4. 如果是,计算指针偏移并返回;否则返回 nullptr
 
// 禁用 RTTI(-fno-rtti)的影响:
// - dynamic_cast 不可用
// - typeid 不可用
// - vtable 中不存储 type_info
// - 减少二进制体积,但丧失运行时类型检查

CRTP 模式(Curiously Recurring Template Pattern)

CRTP 是一种编译期多态技术,将派生类作为模板参数传递给基类,实现静态多态:

// CRTP 基类
template <typename Derived>
class Base {
public:
    void interface() {
        // 静态多态:编译期确定调用 Derived 的实现
        static_cast<Derived*>(this)->implementation();
    }
};
 
class MyClass : public Base<MyClass> {
public:
    void implementation() {
        std::cout << "MyClass implementation" << std::endl;
    }
};
 
// 优势:
// 1. 无虚函数开销(零成本抽象)
// 2. 可以内联(编译器知道具体类型)
// 3. 不需要 vtable/vptr
 
// 实际应用:std::enable_shared_from_this
class Widget : public std::enable_shared_from_this<Widget> {
public:
    std::shared_ptr<Widget> getPtr() {
        return shared_from_this(); // CRTP 提供此方法
    }
};
 
// CRTP 与虚函数的性能对比:
// 虚函数:运行时查找 vtable → 间接调用 → 不可内联
// CRTP:编译期确定类型 → 直接调用 → 可内联
// 在性能关键路径上 CRTP 可快 2-5 倍

类型擦除(Type Erasure)

类型擦除是一种将不同类型统一为同一接口的技术,隐藏具体类型信息:

// 简单的类型擦除实现
class AnyFunction {
    struct Concept {
        virtual ~Concept() = default;
        virtual void invoke() = 0;
        virtual Concept* clone() const = 0;
    };
 
    template <typename F>
    struct Model : Concept {
        F func;
        Model(F f) : func(std::move(f)) {}
        void invoke() override { func(); }
        Concept* clone() const override { return new Model(*this); }
    };
 
    std::unique_ptr<Concept> impl;
 
public:
    template <typename F>
    AnyFunction(F f) : impl(new Model<F>(std::move(f))) {}
 
    AnyFunction(const AnyFunction& other) : impl(other.impl->clone()) {}
    AnyFunction& operator=(const AnyFunction& other) {
        impl.reset(other.impl->clone());
        return *this;
    }
 
    void operator()() { impl->invoke(); }
};
 
// 使用示例:不同类型的可调用对象统一存储
std::vector<AnyFunction> funcs;
funcs.push_back([]{ std::cout << "lambda1"; });
funcs.push_back([]{ std::cout << "lambda2"; });
// 类型被擦除,但行为通过虚函数保留

虚函数默认参数的陷阱

默认参数是静态绑定的——在编译期根据指针/引用的静态类型决定。虚函数本身是动态绑定的:

Base* ptr = new Derived();
ptr->print(10); // 使用 Base::print 的默认参数(10),但调用 Derived::print
// 结论:永远不要在重写的虚函数中改变默认参数值

纯虚函数与抽象基类设计

// 纯虚函数可以有实现(接口分离模式)
class Drawable {
public:
    virtual ~Drawable() = default;
    virtual void draw() const = 0; // 纯虚函数(接口)
protected:
    void logDraw() const { // 纯虚函数带实现
        std::cout << "Drawing..." << std::endl;
    }
};
 
// 抽象基类作为接口(Interface)
class SortStrategy {
public:
    virtual ~SortStrategy() = default;
    virtual void sort(std::vector<int>& data) = 0;
};
 
// 具体实现
class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        std::sort(data.begin(), data.end());
    }
};
 
class BubbleSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        // 冒泡排序实现
    }
};

语法

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 基类 + 不同评价标准的派生类),排序题(抽象比较逻辑)。

练习

题号题目链接知识点
208实现 Trie (前缀树)https://www.luogu.com.cn/problem/P1001多态设计:TrieNode 接口、虚函数实现
P1023ticketshttps://www.luogu.com.cn/problem/P1023语法练习
380O(1) 时间插入、删除和获取随机元素https://www.luogu.com.cn/problem/P1001类封装设计、接口抽象
P1024四则运算https://www.luogu.com.cn/problem/P1024语法练习
460LFU 缓存https://www.luogu.com.cn/problem/P1001策略模式:频率分组的抽象接口
155最小栈https://www.luogu.com.cn/problem/P1001接口设计:辅助栈的虚函数封装
341扁平化嵌套列表迭代器https://www.luogu.com.cn/problem/P1001多态接口:NestedInteger 虚函数
271字符串的编码与解码https://www.luogu.com.cn/problem/P1001接口抽象:编解码器的虚函数设计
297二叉树的序列化与反序列化https://www.luogu.com.cn/problem/P1001设计模式:序列化策略的多态
355设计推特https://www.luogu.com.cn/problem/P1001类继承与接口设计
348设计井字棋https://www.luogu.com.cn/problem/P1001类封装与虚函数的使用
535TinyURL 的加密与解压https://www.luogu.com.cn/problem/P1001设计模式:编码策略的多态接口
139单词拆分https://www.luogu.com.cn/problem/P1001动态规划中的策略选择(可类比策略模式)
212单词搜索 IIhttps://www.luogu.com.cn/problem/P1001Trie + 回溯的组合设计