Skip to content

多态(Polymorphism)实现机制

一、概念:什么是多态?

同一消息作用于不同对象,产生不同行为」。代码上表现为:把一个派生类对象当作基类使用(通过基类指针/引用调用虚函数),实际执行的是派生类自己的版本

cpp
class Shape {
public:
    virtual void draw() const { std::cout << "Shape\n"; }
    virtual ~Shape() = default;
};
class Circle : public Shape {
public:
    void draw() const override { std::cout << "Circle\n"; }
};

Shape* s = new Circle();
s->draw();      // 输出 Circle(动态绑定)——多态 ✨
delete s;       // 调到 Circle::~Circle(因为基类虚析构)✅

二、两种多态

类型机制绑定时间载体
编译期(静态)多态函数重载模板(泛型)编译时重载决议 / 模板实例化
运行期(动态)多态虚函数 + 继承运行时按对象实际类型查虚表vptr/vtable

面试常问区别:哪些是编译期决定、哪些运行期查表;模板多态无 vtable 开销但类型需在编译期确定。

三、运行期多态的实现:虚表 + 虚指针

  • 含虚函数的类对象隐式携带一个 vptr(虚函数表指针),指向该类的虚函数表 vtable
  • vtable 是一张函数指针数组,存放该类所有虚函数的地址(还包括 type_info 等,供 RTTI)。
  • 调用 s->draw():编译成 s->vptr->vtable[offset]() 的间接跳转 —— 这就是动态绑定/晚绑定
  • 派生类继承并覆写虚函数:派生类 vtable 中对应槽位指向派生类版本;未被覆写的继续指向基类版本。

一个例子

class Animal { virtual speak(); virtual eat(); };   // vtable[0]=Animal::speak [1]=Animal::eat
class Dog : public Animal { void speak() override; };// vtable[0]=Dog::speak   [1]=Animal::eat(继承)

Dog 对象 → vptr → Dog 的 vtable;Animal* a = new Dog(); a->speak(); 查到 Dog::speak。

四、高频问答

Q1. 普通虚函数调用开销/为什么慢一点?

多了一次间接寻址(vptr→vtable→函数),且阻碍编译器内联。但现代 CPU 分支预测与虚调用缓存让开销通常很小;热路径可用 final/模板规避虚调用。

Q2. 静态绑定 vs 动态绑定?

  • 非虚(或通过对象a.speak() 访问):编译期绑定调用基类版本。
  • 虚且通过指针/引用:运行期查 vtable 动态绑定。
  • 经典坑:在构造/析构函数内调用虚函数静态绑定(调用本类版本,不触发多态)——因为构造时 vptr 还指向正在构造那层基类的 vtable。

Q3. inline + virtual 能共存吗?final / override 关键字的用途?

  • inline 只是建议内联,虚函数通常不被内联(间接调用难内联),但在能静态确认目标时可内联(非多态使用)。
  • override:显式标记覆写,编译器检查确实覆写了基类虚函数(拼错/签名不符报错)。
  • final:禁止该类再被继承 / 禁止某虚函数再被覆写,也让编译期更易去虚化(devirtualization)优化。

Q4. private/protected virtual 与多态的关系?

虚函数访问控制不阻止调用,决定调用语义的是静态类型下的可见性 + 动态分派。protected/private 虚函数也能被覆写,外部通过基类指针不可直接调用私有虚等(作为「接口留钩子」设计)。

五、手写/示意动态调用

面试描述「用函数指针模拟多态」能体现理解:

cpp
struct VTable;  // 概念
struct AnimalVtbl { void (*speak)(Animal*); };
// Animal 对象布局: [vptr][data]

这其实就是 C 语言用「结构体 + 函数指针表」实现对象模型的方式,能答出「vtable 本质是函数指针数组」即为理解到位。

C++ 面试八股 · VitePress 版