虚函数、vtable 与多态
深入运行时多态:虚分派在底层如何工作、何时使用 override 和 final、抽象类以及 RTTI。
运行时多态
多态意味着「多种形态」 ── 相同的接口根据对象的实际类型表现出不同的行为。在 C++ 中,运行时多态通过虚函数实现。当你通过基类指针或引用调用虚函数时,调用在运行时被分派到实际派生类的实现。
这就是使面向对象编程强大的机制:你可以编写操作基类接口的代码,它会自动正确地与任何派生类协作 ── 即使是在你的代码之后编写的派生类。
vtable 的工作原理
当一个类拥有虚函数时,编译器会创建一个 vtable(虚函数表) ── 一个函数指针数组。该类的每个对象包含一个隐藏的 vptr(指向 vtable 的指针)。当虚函数被调用时,程序沿着 vptr 找到 vtable,查找函数指针,然后通过它进行调用。这种间接调用就是虚函数调用相比直接调用有少量开销的原因。
#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;
}虚析构函数是必需的
如果一个类有任何虚函数,其析构函数必须是虚的。否则,通过基类指针删除派生对象会导致未定义行为 ── 派生类的析构函数永远不会运行,从而泄漏资源。
#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 关键字阻止函数被进一步重写,或阻止类被继承。
#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)模式提供一个公共的非虚入口点,该入口点调用一个私有虚函数,让基类控制前置/后置条件。
#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 安全地将基类指针转换为派生类指针,如果转换失败则返回 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. 为什么应该避免在构造函数中调用虚函数?