虚函数与多态底层
虚函数是 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 关键字帮助编译器去虚化。析构函数在多态基类中必须是虚函数。