跳至内容

虚函数、vtable 与多态

深入运行时多态:虚分派在底层如何工作、何时使用 override 和 final、抽象类以及 RTTI。

运行时多态

多态意味着「多种形态」 ── 相同的接口根据对象的实际类型表现出不同的行为。在 C++ 中,运行时多态通过虚函数实现。当你通过基类指针或引用调用虚函数时,调用在运行时被分派到实际派生类的实现。

这就是使面向对象编程强大的机制:你可以编写操作基类接口的代码,它会自动正确地与任何派生类协作 ── 即使是在你的代码之后编写的派生类。

vtable 的工作原理

当一个类拥有虚函数时,编译器会创建一个 vtable(虚函数表) ── 一个函数指针数组。该类的每个对象包含一个隐藏的 vptr(指向 vtable 的指针)。当虚函数被调用时,程序沿着 vptr 找到 vtable,查找函数指针,然后通过它进行调用。这种间接调用就是虚函数调用相比直接调用有少量开销的原因。

polymorphism.cpp
#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual ~Shape() = default;

    // Pure virtual — no default implementation
    virtual double area() const = 0;

    // Virtual with default implementation
    virtual std::string name() const { return "Shape"; }
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_{r} {}

    double area() const override {
        return 3.14159265 * radius_ * radius_;
    }

    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_{w}, h_{h} {}

    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
};

// Works with ANY Shape — past, present, or future
void print_info(const Shape& s) {
    std::cout << s.name() << ": area = " << s.area() << '\n';
}

int main() {
    Circle c{5.0};
    Rectangle r{3.0, 4.0};

    print_info(c);  // Circle: area = 78.5398
    print_info(r);  // Rectangle: area = 12

    // Polymorphic container
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(2.0));
    shapes.push_back(std::make_unique<Rectangle>(6.0, 3.0));

    for (const auto& s : shapes) {
        print_info(*s);  // virtual dispatch at runtime
    }
    return 0;
}

虚析构函数是必需的

如果一个类有任何虚函数,其析构函数必须是虚的。否则,通过基类指针删除派生对象会导致未定义行为 ── 派生类的析构函数永远不会运行,从而泄漏资源。

virtual_destructor.cpp
#include <iostream>
#include <memory>

class Base {
public:
    // Without virtual destructor: UNDEFINED BEHAVIOR on delete
    // virtual ~Base() = default;  // ALWAYS do this!
    ~Base() { std::cout << "~Base\n"; }
};

class Derived : public Base {
    int* data_;
public:
    Derived() : data_{new int[1000]} {
        std::cout << "Derived: allocated\n";
    }
    ~Derived() {
        delete[] data_;
        std::cout << "~Derived: freed\n";
    }
};

int main() {
    Base* ptr = new Derived{};
    delete ptr;  // Only ~Base runs! ~Derived is NOT called!
    // Result: memory leak (data_ never freed)
    // Fix: make ~Base() virtual
    return 0;
}

override 与 final 关键字

override 关键字告诉编译器你打算重写基类的虚函数。如果签名不匹配(常见的拼写错误),编译器会捕获它。final 关键字阻止函数被进一步重写,或阻止类被继承。

override_final.cpp
#include <iostream>

class Base {
public:
    virtual ~Base() = default;
    virtual void process(int x) const { std::cout << "Base: " << x << '\n'; }
    virtual void execute() { std::cout << "Base::execute\n"; }
};

class Middle : public Base {
public:
    // override: compiler checks signature matches Base::process
    void process(int x) const override {
        std::cout << "Middle: " << x * 2 << '\n';
    }

    // final: no further derived class can override execute()
    void execute() final { std::cout << "Middle::execute\n"; }
};

class Bottom : public Middle {
public:
    void process(int x) const override {
        std::cout << "Bottom: " << x * 3 << '\n';
    }

    // ERROR if uncommented: execute() is final in Middle
    // void execute() override { }
};

// A final class cannot be inherited from at all
class Leaf final : public Base {
public:
    void process(int x) const override {
        std::cout << "Leaf: " << x << '\n';
    }
};

// ERROR if uncommented: cannot inherit from final class
// class Attempt : public Leaf {};

int main() {
    Bottom b;
    Base& ref = b;
    ref.process(10);  // Bottom: 30 (virtual dispatch)
    ref.execute();    // Middle::execute (final version)
    return 0;
}

抽象类与 NVI 模式

具有至少一个纯虚函数(= 0)的类是抽象的 ── 它不能被实例化。非虚接口(NVI)模式提供一个公共的非虚入口点,该入口点调用一个私有虚函数,让基类控制前置/后置条件。

nvi_pattern.cpp
#include <iostream>
#include <string>

// NVI Pattern: public non-virtual calls private virtual
class Validator {
public:
    virtual ~Validator() = default;

    // Public non-virtual interface — controls the workflow
    bool validate(const std::string& input) const {
        if (input.empty()) {
            std::cout << "[Validator] Input is empty\n";
            return false;
        }
        // Delegate to derived class implementation
        bool result = do_validate(input);
        std::cout << "[Validator] " << (result ? "PASS" : "FAIL") << '\n';
        return result;
    }

private:
    // Pure virtual — derived classes MUST implement
    virtual bool do_validate(const std::string& input) const = 0;
};

class EmailValidator : public Validator {
    bool do_validate(const std::string& input) const override {
        return input.find('@') != std::string::npos
            && input.find('.') != std::string::npos;
    }
};

class LengthValidator : public Validator {
    std::size_t min_len_;
    bool do_validate(const std::string& input) const override {
        return input.length() >= min_len_;
    }
public:
    explicit LengthValidator(std::size_t min) : min_len_{min} {}
};

int main() {
    EmailValidator email_v;
    LengthValidator len_v{5};

    email_v.validate("user@example.com");  // PASS
    email_v.validate("invalid");            // FAIL
    len_v.validate("hi");                   // FAIL
    len_v.validate("hello!");               // PASS
    return 0;
}

RTTI 与 dynamic_cast

RTTI(运行时类型信息) 让你可以在运行时查询多态对象的实际类型。dynamic_cast(base_ptr) 安全地将基类指针转换为派生类指针,如果转换失败则返回 nullptr。对于引用,失败的 dynamic_cast 会抛出 std::bad_cast。

typeid(obj) 返回一个 std::type_info 引用,可用于比较以识别运行时类型。

应谨慎使用 RTTI ── 频繁使用 dynamic_cast 通常表明设计存在问题。优先使用虚函数来实现类型特定的行为。

常见陷阱

永远不要在构造函数或析构函数中调用虚函数。在构造期间,对象的动态类型是当前正在构造的类,而不是最终的派生类。在 Base::Base() 中的虚调用将调用 Base::method(),而不是 Derived::method() ── 即使正在创建的对象是 Derived。这是一个众所周知的错误来源,可能极难诊断。

关键要点
  • 虚函数通过 vtable 和 vptr 实现运行时多态
  • 任何有虚函数的类必须有一个 virtual 析构函数
  • 重写时始终使用 override ── 它在编译时捕获签名不匹配
  • 纯虚函数(= 0)使类变为抽象并强制派生类实现它们
  • NVI 模式让基类在自定义点周围强制执行前置/后置条件
  • 避免在构造函数/析构函数中调用虚函数 ── 派生类尚未(或已不再)完全构造

测验 — 检验你的知识

(20 经验值)

1. 如果多态基类没有虚析构函数会怎样?

2. `override` 关键字的作用是什么?

3. 为什么应该避免在构造函数中调用虚函数?