C++ 虚函数全解:从基础原理到高级特性(多重继承 / 菱形继承 / CRTP 对比)

前言

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

一、什么是虚函数

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

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

1.1 虚函数三大特点

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

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

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

cpp

Animal *p = new Dog();

// 父类指针指向子类对象,触发多态

二、虚函数实现原理

2.1 虚函数表 vtable + 虚指针 vptr

虚函数表 vtable

由编译器自动生成

每个有虚函数的类,独有一张虚表

存放当前类所有虚函数的地址

所有同类型对象共用同一张虚表

虚指针 vptr

每个对象独有一个 vptr

指向当前类的虚函数表

普通对象:只有成员变量

含虚函数对象:成员变量 + vptr 指针

示例:

cpp

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

B b1, b2; // 2个对象,共用B同一张虚表

2.2 虚函数调用过程

通过对象找到自身 vptr

通过 vptr 找到所属类的 vtable

在虚表中取出虚函数地址

跳转执行函数

三、虚函数关键特性

3.1 覆盖 override 和 隐藏 hiding

覆盖(Override):有 virtual,运行时动态绑定,父类指针调子类实现,真正多态

隐藏(Hiding):无 virtual,编译期静态绑定,父类调子类、子类调子类,互不干扰,不触发多态

3.2 final 关键字 C++11

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

cpp

class Base {

public:

virtual void foo() final {}

};

class Derived : public Base {

// 编译报错,无法重写final虚函数

void foo() {}

};

修饰类:禁止被继承

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

class Base {

public:

void interface() {

static_cast(this)->impl();

}

};

class Derived : public Base {

public:

void impl() {

// 子类具体实现

}

};

7.2 CRTP 与虚函数对比

表格

特性

虚函数

CRTP

绑定时机

运行时动态绑定

编译期静态绑定

运行开销

有 vptr、虚表、间接调用

零开销

多态类型

运行时多态

编译期多态

适用场景

框架、动态类型切换

高性能、底层复用

八、总结

虚函数依靠 vptr + vtable 实现运行时多态;每个类一张虚表,每个对象一个虚指针。

override 是覆盖多态,无 virtual 是名字隐藏,不触发多态。

final 可禁止重写、禁止继承;纯虚函数构成抽象类,只能做接口。

基类析构必须虚析构,防止内存泄漏。

构造函数内虚函数无效;虚函数不要用默认参数,静态绑定会挖坑。

多重继承有几个带虚函数基类,就有几个 vptr。

菱形继承用虚继承解决冗余二义性,最终类必须重写冲突虚函数。

要动态多态用虚函数,要高性能无开销多态用 CRTP。

拼音查詢
热切期待,'迷你世界开服日期'公布在即