Skip to content

虚函数与多态底层

虚函数是 C++ 运行时多态的基础。理解 vtable 机制,才能知道多态的代价,以及何时该用、何时不该用。

虚函数表(vtable)机制

含虚函数的类对象内存布局:

┌─────────────────────────────┐
│  vptr(虚函数表指针,8字节)  │  ← 对象首地址
├─────────────────────────────┤
│  成员变量 1                  │
│  成员变量 2                  │
│  ...                        │
└─────────────────────────────┘


┌─────────────────────────────┐  vtable(只读数据段,每个类一份)
│  type_info* (RTTI 信息)    │
│  virtual_func_1 地址         │
│  virtual_func_2 地址         │
│  ...                        │
└─────────────────────────────┘
cpp
#include <iostream>

class Animal {
public:
    virtual void speak() { std::cout << "...\n"; }
    virtual void move()  { std::cout << "moving\n"; }
    virtual ~Animal() = default;

    int age = 0;  // 普通成员
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof!\n"; }
    // move() 未重写,继承 Animal::move
};

// 内存布局验证
Animal a;
Dog d;

// sizeof 包含 vptr
std::cout << sizeof(Animal) << "\n";  // 16(vptr 8 + int 4 + padding 4)
std::cout << sizeof(Dog) << "\n";     // 16(相同,Dog 没有新成员)

// 多态调用
Animal* p = new Dog();
p->speak();  // 输出 "Woof!",运行时查 vtable
delete p;    // 调用 Dog::~Dog(因为析构是虚函数)

虚函数调用的汇编

cpp
// 虚函数调用的底层步骤:
// 1. 从对象首地址读取 vptr
// 2. 从 vtable 中读取函数地址(按偏移量)
// 3. 间接调用

// 伪代码等价:
void call_speak(Animal* p) {
    // p->speak() 等价于:
    void** vtable = *reinterpret_cast<void***>(p);  // 读 vptr
    auto func = reinterpret_cast<void(*)(Animal*)>(vtable[1]);  // 读函数指针
    func(p);  // 间接调用
}

// 非虚函数调用:直接调用,无间接寻址
// 虚函数调用:2次内存读取 + 1次间接跳转
// 开销:约 1-3 ns(现代 CPU 分支预测可以缓解)

继承与 vtable 布局

cpp
class Base {
public:
    virtual void f1() { std::cout << "Base::f1\n"; }
    virtual void f2() { std::cout << "Base::f2\n"; }
    int x = 1;
};

class Derived : public Base {
public:
    void f1() override { std::cout << "Derived::f1\n"; }  // 覆盖
    virtual void f3() { std::cout << "Derived::f3\n"; }   // 新增
    int y = 2;
};

// Base vtable:    [f1_base, f2_base]
// Derived vtable: [f1_derived, f2_base, f3_derived]
//                  ↑ 覆盖       ↑ 继承    ↑ 新增

// 对象布局:
// Base:    [vptr | x]
// Derived: [vptr | x | y]  ← vptr 指向 Derived 的 vtable

多重继承与虚函数

cpp
class A {
public:
    virtual void fa() {}
    int a = 1;
};

class B {
public:
    virtual void fb() {}
    int b = 2;
};

class C : public A, public B {
public:
    void fa() override {}
    void fb() override {}
    int c = 3;
};

// C 的内存布局:
// [vptr_A | a | vptr_B | b | c]
//    ↑ A 的 vtable 部分    ↑ B 的 vtable 部分

// 指针转换时地址会调整!
C obj;
A* pa = &obj;  // pa == &obj(偏移 0)
B* pb = &obj;  // pb == &obj + sizeof(A)(偏移调整)

std::cout << (void*)&obj << "\n";
std::cout << (void*)pa << "\n";   // 相同
std::cout << (void*)pb << "\n";   // 不同!

虚析构函数

cpp
// 没有虚析构函数的危险
class Base {
public:
    ~Base() { std::cout << "Base 析构\n"; }  // 非虚!
};

class Derived : public Base {
    int* data = new int[100];
public:
    ~Derived() {
        delete[] data;
        std::cout << "Derived 析构\n";
    }
};

Base* p = new Derived();
delete p;  // 只调用 Base::~Base,内存泄漏!

// 正确:基类析构函数必须是虚函数
class SafeBase {
public:
    virtual ~SafeBase() = default;  // 虚析构
};

纯虚函数与抽象类

cpp
class Shape {
public:
    virtual double area() const = 0;    // 纯虚函数
    virtual double perimeter() const = 0;

    // 纯虚函数也可以有实现(但子类必须重写)
    virtual void describe() const = 0;

    virtual ~Shape() = default;
};

void Shape::describe() const {
    std::cout << "I am a shape\n";
}

class Circle : public Shape {
    double r_;
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return 3.14159 * r_ * r_; }
    double perimeter() const override { return 2 * 3.14159 * r_; }
    void describe() const override {
        Shape::describe();  // 调用基类实现
        std::cout << "radius=" << r_ << "\n";
    }
};

// Shape s;  // 编译错误!抽象类不能实例化
Shape* s = new Circle(5.0);  // OK,多态

override 与 final

cpp
class Base {
public:
    virtual void foo(int x) {}
    virtual void bar() {}
};

class Derived : public Base {
public:
    void foo(int x) override {}  // 明确标记重写,编译器检查签名
    // void foo(double x) override {}  // 编译错误!签名不匹配

    void bar() final {}  // 禁止子类再次重写
};

class Final final : public Derived {  // 禁止继承
    // void bar() override {}  // 编译错误!bar 是 final
};
// class Sub : public Final {};  // 编译错误!Final 是 final 类

性能优化:去虚化

cpp
// 编译器可以对虚函数调用进行去虚化(devirtualization)
// 当对象类型在编译期已知时

void process(Circle& c) {
    c.area();  // 编译器知道是 Circle,可能直接调用,不查 vtable
}

// 使用 final 帮助编译器去虚化
class FastCircle final : public Shape {
    double area() const override { return 3.14159 * r_ * r_; }
};

// CRTP(奇异递归模板模式):编译期多态,零虚函数开销
template<typename Derived>
class ShapeBase {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Square : public ShapeBase<Square> {
    double side_;
public:
    double area_impl() const { return side_ * side_; }
};

关键认知

虚函数的开销主要来自间接调用和 cache miss,而非函数调用本身。对性能敏感的热路径,考虑 CRTP 或 final 关键字帮助编译器去虚化。析构函数在多态基类中必须是虚函数。

系统学习 C++ 生态,深入底层架构