静态多态靠编译期绑定,本质是函数重载和模板;动态多态需虚函数、基类指针/引用、运行时对象三要素;虚函数表是编译器实现机制;纯虚函数用于定义抽象接口。

静态多态靠编译期绑定,本质是函数重载和模板
静态多态在编译时就确定调用哪个函数,不依赖对象实际类型。最常见的是函数重载和函数模板,编译器根据参数类型、个数或模板实参直接生成对应版本的代码。
容易踩的坑:operator+ 重载时若参数类型不匹配(比如左值是 int,右值是自定义类),编译器可能找不到合适重载;模板推导失败时不会报“重载歧义”,而是直接报“no matching function”。
示例:
templateT add(T a, T b) { return a + b; } void func(int x) { } void func(double x) { } // 重载,非虚函数
add(3, 5); // 编译期生成 int 版本 add(3.14, 2.7); // 编译期生成 double 版本 func(42); // 调用 void func(int)
动态多态必须有虚函数+基类指针/引用+运行时对象
动态多态的核心是运行时根据对象实际类型决定调用哪个函数,三要素缺一不可:基类中声明 virtual 函数、用基类指针或引用指向派生类对象、派生类重写该函数(用 override 显式标注更安全)。
立即学习“C++免费学习笔记(深入)”;
常见错误现象:
- 忘记加
virtual—— 编译通过但调用的是基类版本(静态绑定) - 用对象值传递(如
Base b = Derived();)—— 发生切片,虚函数表丢失 - 析构函数没声明为
virtual——delete基类指针时派生类析构函数不执行
示例:
struct Base {
virtual ~Base() = default; // 必须 virtual
virtual void say() { std::cout << "Base"; }
};
struct Derived : Base {
void say() override { std::cout << "Derived"; }
};
Base* p = new Derived();
p->say(); // 输出 "Derived",动态绑定生效
虚函数表(vtable)不是语法,而是实现机制
虚函数表是编译器生成的内部结构,每个含虚函数的类有一个 vtable,每个对象头包含一个指向 vtable 的指针(vptr)。它决定了动态绑定如何工作,但你不需手动操作它。
影响与注意点:
- 虚函数带来轻微开销:每次调用需查 vtable(现代 CPU 分支预测缓解明显)
- 类大小变化:含虚函数的类比不含的多一个指针大小(通常 8 字节)
-
sizeof(Base)可能 ≠sizeof(Derived),尤其当后者新增成员变量 - 构造函数中调用虚函数不会触发动态绑定(此时 vptr 还未完全初始化)
所以面试问“vtable 是什么”,答“编译器为支持动态多态生成的函数指针数组,用于运行时分发虚函数调用”就够了,不必深究 ABI 细节。
纯虚函数和抽象类是接口设计的关键手段
声明 virtual void foo() = 0; 表示该函数无实现且强制派生类覆盖。含至少一个纯虚函数的类叫抽象类,不能实例化,只能作为接口被继承。
典型使用场景:
- 统一管理不同设备驱动(
Device::read()、Device::write()) - 策略模式中封装算法变体(
Strategy::execute()) - 避免“空实现”污染基类(相比提供默认空函数,纯虚更明确意图)
注意:= 0 必须写在声明处(头文件内),不能在定义里补实现(除非是纯虚析构函数,它必须有定义)。
示例:
struct Shape {
virtual double area() const = 0; // 纯虚
virtual ~Shape() = default; // 抽象类也应有 virtual 析构
};
struct Circle : Shape {
double r;
double area() const override { return 3.1416 r r; }
};
// Shape s; // 错误:不能实例化抽象类
动态多态的可靠性高度依赖对象生命周期和指针/引用的正确使用,很多崩溃问题其实不是虚函数本身的问题,而是对象提前析构、指针悬挂或切片导致的 vptr 失效。











