前言

虚函数是 C++运行时多态的核心,也是面试、底层开发、嵌入式 C++ 必考重难点。本文从零梳理:定义、特点、底层原理、覆盖与隐藏、final、纯虚函数、虚析构、构造虚函数、默认参数坑、多重继承内存模型、菱形继承,最后对比 CRTP 静态多态,全篇适合直接背诵 + 面试默写。

一、什么是虚函数

虚函数由 virtual 关键字修饰,是 C++ 实现运行时多态的核心机制。

通俗理解父类指针 / 引用当入口,程序运行时,实际自动执行子类重写的函数版本。

1.1 虚函数三大特点

  1. 多态绑定运行时按照对象实际类型调用对应函数,而不是指针的声明类型。理解:继承父类接口行为,但是实现自己独有逻辑。

  2. 继承性派生类可以使用 override 显式重写基类虚函数。

  3. 多态调用必须通过基类指针 / 基类引用才能触发多态。

cpp

Animal *p = new Dog();  
// 父类指针指向子类对象,触发多态

二、虚函数实现原理

2.1 虚函数表 vtable + 虚指针 vptr

  1. 虚函数表 vtable
  • 由编译器自动生成
  • 每个有虚函数的类,独有一张虚表
  • 存放当前类所有虚函数的地址
  • 所有同类型对象共用同一张虚表
  1. 虚指针 vptr
  • 每个对象独有一个 vptr
  • 指向当前类的虚函数表
  • 普通对象:只有成员变量
  • 含虚函数对象:成员变量 + vptr 指针

示例:

cpp

A a1, a2, a3;   // 3个对象,共用A同一张虚表
B b1, b2;       // 2个对象,共用B同一张虚表

2.2 虚函数调用过程

  1. 通过对象找到自身 vptr
  2. 通过 vptr 找到所属类的 vtable
  3. 在虚表中取出虚函数地址
  4. 跳转执行函数

三、虚函数关键特性

3.1 覆盖 override 和 隐藏 hiding

  • 覆盖(Override):有 virtual,运行时动态绑定,父类指针调子类实现,真正多态
  • 隐藏(Hiding):无 virtual,编译期静态绑定,父类调子类、子类调子类,互不干扰,不触发多态

3.2 final 关键字 C++11

  1. 修饰虚函数:禁止子类重写

cpp

class Base {
public:
    virtual void foo() final {}
};
class Derived : public Base {
    // 编译报错,无法重写final虚函数
    void foo() {}  
};
  1. 修饰类:禁止被继承

cpp

class Base final{};
class Derived : public Base{}; // 编译报错

3.3 纯虚函数与抽象类

纯虚函数语法

cpp

virtual double area() = 0; 

只有声明、无函数体。

抽象类包含至少一个纯虚函数的类:

  • 不能实例化对象
  • 只能作为接口模板被继承
  • 子类必须全部重写纯虚函数,否则子类也变成抽象类

表格

类型 语法 是否抽象类 能否实例化 子类是否必须重写
普通虚函数 virtual void f(){} 可以 可选,不重写用父类
纯虚函数 virtual void f() = 0; 不可以 必须重写

四、虚函数性能分析

4.1 开销来源

空间开销

  • 每个对象增加一个 vptr 4/8 字节
  • 每个类单独占用一张虚函数表空间

时间开销

  • 运行时间接调用,多 1~2 次寻址
  • 破坏编译器优化,难以函数内联 inline

4.2 性能优化建议

  • 性能关键路径少用虚函数
  • 无多态需求不要定义 virtual
  • 追求高性能多态改用 CRTP 编译期静态多态

五、虚函数高级技术

5.1 虚析构函数

核心结论:基类析构必须加 virtual,否则基类指针 delete 子类对象时,只调用基类析构,子类析构不执行,造成内存泄漏

cpp

class Base {
public:
    virtual ~Base() {}  // 必须虚析构
};

class Derived : public Base {
    ~Derived() override {}
};

Base* p = new Derived();
delete p; 
// 正确:先析构子类,再析构父类

5.2 构造函数中的虚函数

构造函数执行阶段,虚函数机制还没完全建立,只会调用当前自己类的函数版本,不会触发多态。

5.3 虚函数默认参数

  • 虚函数:运行时动态绑定
  • 默认参数:编译期静态绑定,编译阶段直接替换值

坑点:调用的是子类函数,用的却是父类默认参数。开发规范:虚函数尽量不要设置默认参数。

六、虚函数与对象内存模型

6.1 多重继承下的虚函数

场景:Derived 同时继承 Base1、Base2,两个基类都有虚函数。

内存布局:

plaintext

Derived 对象内存:
┌───────────────────────────┐
│  vptr1 → Base1 vtable     │  
├───────────────────────────┤
│  vptr2 → Base2 vtable     │  
└───────────────────────────┘

口诀:对象里有几个带虚函数的基类,就有几个 vptr;逻辑上每个基类对应一张独立虚函数表。

6.2 菱形继承 虚继承

继承结构:

plaintext

        Base
       /    \
Derived1   Derived2
       \    /
       Final

普通继承问题:Final 会包含两份 Base 子对象,造成冗余和二义性。解决:使用 virtual 虚继承,保证最终类只保留一份共享 Base。

如果 Derived1、Derived2 都重写了 Base 的虚函数,Final 必须自己重写该虚函数,否则编译器不知道选用哪一个父类版本,产生歧义报错。

七、CRTP 奇异递归模板模式

7.1 核心概念

CRTP 是编译期静态多态,代替虚函数,无虚表、无运行时开销

写法:子类把自己当做模板参数传给父类。

cpp

运行

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->impl();
    }
};

class Derived : public Base<Derived> {
public:
    void impl() {
        // 子类具体实现
    }
};

7.2 CRTP 与虚函数对比

表格

特性 虚函数 CRTP
绑定时机 运行时动态绑定 编译期静态绑定
运行开销 有 vptr、虚表、间接调用 零开销
多态类型 运行时多态 编译期多态
适用场景 框架、动态类型切换 高性能、底层复用

八、总结

  1. 虚函数依靠 vptr + vtable 实现运行时多态;每个类一张虚表,每个对象一个虚指针。
  2. override 是覆盖多态,无 virtual 是名字隐藏,不触发多态。
  3. final 可禁止重写、禁止继承;纯虚函数构成抽象类,只能做接口。
  4. 基类析构必须虚析构,防止内存泄漏。
  5. 构造函数内虚函数无效;虚函数不要用默认参数,静态绑定会挖坑。
  6. 多重继承有几个带虚函数基类,就有几个 vptr。
  7. 菱形继承用虚继承解决冗余二义性,最终类必须重写冲突虚函数。
  8. 要动态多态用虚函数,要高性能无开销多态用 CRTP。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐