📣📣📣📣📣📣📣📣 ✏️作者主页:瑶池酒剑仙 📋 系列专栏:C++基础宝典 🌲上一篇: C++继承与派生完全指南:单继承/多继承、菱形继承、虚基类与访问控制 🌲下一篇: C++虚函数表(vtable)深度剖析:多态的内存实现原理与性能代价 📣📣📣📣📣📣📣📣
🎍逐梦编程,让中华屹立世界之巅。 🎍简单的事情重复做,重复的事情用心做,用心的事情坚持做;

文章目录
- 引言:从一次"改了没生效"的 bug 说起
- 一、为什么需要虚函数
-
- 1.1 一个没有虚函数的例子
- 1.2 这个默认行为的代价
- 1.3 虚函数登场
- 二、virtual 关键字的使用
-
- 2.1 基本语法
- 2.2 多态触发的三个条件
- 2.3 值传递会"丢失"多态
- 2.4 虚函数不能是某些特殊函数
- 三、override 关键字(C++11)
-
- 3.1 为什么需要 override
- 3.2 override 的检查机制
- 3.3 override 与重载、隐藏的区别
- 四、final 关键字
-
- 4.1 final 修饰虚函数
- 4.2 final 修饰类
- 4.3 final 的性能含义
- 4.4 final 和 override 能一起用吗
- 五、虚函数与重载、隐藏、覆盖(深入)
-
- 5.1 派生类函数隐藏基类所有同名重载
- 5.2 虚函数 + 隐藏的组合陷阱
- 六、虚函数的返回值
-
- 6.1 返回值类型必须兼容
- 6.2 协变返回类型
- 七、虚函数的常见误区与特殊场景
-
- 7.1 私有虚函数
- 7.2 虚函数与 inline
- 7.3 虚函数与默认参数
- 7.4 虚函数与 `static_cast` / `dynamic_cast`
- 八、综合实战:一个基础图形绘制系统
- 九、总结与下一篇预告
-
- 9.1 虚函数核心结论
- 9.2 八条实战军规
- 9.3 练习题
- 9.4 下一篇预告
- 新壁纸(青雀黄龙之舳)
引言:从一次"改了没生效"的 bug 说起
几年前做音视频相关的项目,写了这样一段代码:
class Renderer {
public:
void render() {
doRender();
}
void doRender() {
cout << "Base render" << endl;
}
};
class OpenGLRenderer : public Renderer {
public:
void doRender() { // 想重写基类的行为
cout << "OpenGL render" << endl;
}
};
int main() {
OpenGLRenderer gl;
Renderer& r = gl;
r.render(); // 我期待输出 "OpenGL render"
}
实际输出是 Base render。当时我以为"派生类定义了同名函数,基类调用它时自然就用派生类的版本"——这是很多人从 Java、Python 带过来的思维惯性。后来才知道,C++ 默认是静态绑定(编译期决定调哪个函数),想要动态绑定(运行期根据实际类型决定),必须给基类函数加上 virtual 关键字。
那次之后我认真研究了虚函数。我发现虚函数这个话题,表面语法很简单(就是加个 virtual),但真正理解它需要回答好几个问题:
- 为什么加了 virtual 就能动态绑定了?底层发生了什么?
- 为什么"通过指针/引用"能多态,"通过对象"就不行?
- override 关键字有什么用,C++11 之前没它是怎么过的?
- final 和虚函数什么关系?
- 为什么派生类同名函数有时候"覆盖"有时候"隐藏"?这俩词什么区别?
- 为什么构造函数不能是虚函数,析构函数却"应该是"虚函数?
这一篇,我专门聚焦虚函数的基础机制,也就是"怎么定义、怎么用、override 和 final 怎么配合",把上面这些问题从语义层面讲透。虚函数表的底层实现(vtable、vptr)和虚析构函数的深入讨论,我会放到后面两篇单独讲——因为它们值得独立成篇,混在一起反而讲不透。
代码全部在 GCC / Clang 上编译验证过,涉及"可能误判"的地方我会明确标注输出和原因。
一、为什么需要虚函数
1.1 一个没有虚函数的例子
先不看虚函数,看看 C++ 默认的行为是什么。
代码如下(示例):
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
#include <iostream>
#include <string>
using namespace std;
// 没有虚函数的基类
class Animal {
public:
void speak() const {
cout << "Animal 发出声音" << endl;
}
};
class Dog : public Animal {
public:
void speak() const { // 和基类同名,但没有 virtual
cout << "Dog 汪汪汪" << endl;
}
};
class Cat : public Animal {
public:
void speak() const {
cout << "Cat 喵喵喵" << endl;
}
};
void noVirtualDemo() {
cout << "=== 没有虚函数 ===" << endl;
Dog dog;
Cat cat;
// 直接通过对象调用:调用的是各自类的版本
dog.speak();
cat.speak();
// 关键场景:通过基类引用/指针调用
Animal& ref1 = dog;
Animal& ref2 = cat;
ref1.speak(); // 输出什么?
ref2.speak(); // 输出什么?
Animal* p1 = &dog;
Animal* p2 = &cat;
p1->speak();
p2->speak();
}
输出结果:
=== 没有虚函数 ===
Dog 汪汪汪
Cat 喵喵喵
Animal 发出声音
Animal 发出声音
Animal 发出声音
Animal 发出声音
关键观察:直接通过对象调用时,调用的是各自类的版本。但通过基类引用或指针调用时,调用的却是基类的版本,完全忽略了对象的实际类型。
这就是 C++ 的默认行为:静态绑定(static binding / early binding)。编译器在编译期根据静态类型(变量声明时的类型)决定调哪个函数。Animal& ref1 的静态类型是 Animal&,所以调 Animal::speak。
1.2 这个默认行为的代价
上面的行为在很多场景下是"正确"的。比如你只想复用基类的代码、不涉及多态,那就没问题。但面向对象编程最核心的诉求之一就是多态——“用统一的接口处理不同类型的对象”,这正是上面这段代码做不到的。
考虑一个真实的场景:一个绘图程序,有几百种图形(圆、矩形、三角形……),每种都能算面积、能绘制。如果不能用多态,代码会变成什么样子?
// 没有多态的痛苦版本(不要这样写)
enum class ShapeType { Circle, Rectangle, Triangle };
struct Shape {
ShapeType type;
double a, b, c; // 各种参数
};
double area(const Shape& s) {
switch (s.type) {
case ShapeType::Circle: return 3.14159 * s.a * s.a;
case ShapeType::Rectangle: return s.a * s.b;
case ShapeType::Triangle: return 0.5 * s.a * s.b;
}
return 0;
}
void draw(const Shape& s) {
switch (s.type) {
case ShapeType::Circle: /* 画圆 */ break;
case ShapeType::Rectangle: /* 画矩形 */ break;
case ShapeType::Triangle: /* 画三角形 */ break;
}
}
这种写法有几个致命问题:
多态就是为了解决这个问题——把行为绑定到类型上,让编译器在运行期根据实际类型自动选择正确的方法。
1.3 虚函数登场
虚函数就是 C++ 提供的多态机制。给基类的函数加上 virtual,派生类重写它,然后通过基类指针/引用调用,就会动态绑定到实际对象的类型上。
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
class AnimalV {
public:
virtual void speak() const { // virtual 关键字
cout << "AnimalV 发出声音" << endl;
}
virtual ~AnimalV() = default; // 后面会讲为什么
};
class DogV : public AnimalV {
public:
void speak() const override { // C++11 推荐加 override
cout << "DogV 汪汪汪" << endl;
}
};
class CatV : public AnimalV {
public:
void speak() const override {
cout << "CatV 喵喵喵" << endl;
}
};
void virtualBasicDemo() {
cout << "\\n=== 虚函数基础 ===" << endl;
DogV dog;
CatV cat;
// 通过基类引用调用:动态绑定
AnimalV& ref1 = dog;
AnimalV& ref2 = cat;
ref1.speak(); // 输出 "DogV 汪汪汪"
ref2.speak(); // 输出 "CatV 喵喵喵"
// 通过基类指针调用:动态绑定
AnimalV* p1 = &dog;
AnimalV* p2 = &cat;
p1->speak(); // 输出 "DogV 汪汪汪"
p2->speak(); // 输出 "CatV 喵喵喵"
}
只加了一个 virtual,行为就完全不同了。这就是动态绑定(dynamic binding / late binding):编译器不再在编译期决定调哪个函数,而是生成代码在运行期根据对象的虚函数表来查正确的函数地址。
(虚函数表是什么,怎么工作的,下一篇专门讲。这一篇我们先把语义层面讲透。)
二、virtual 关键字的使用
2.1 基本语法
虚函数在基类中声明时加 virtual,派生类重写时可以加也可以不加,但推荐加 override(C++11 起)。
class Base {
public:
// 声明为虚函数
virtual void func() {
cout << "Base::func" << endl;
}
// 虚析构函数(基类如果会被多态 delete,必须这样声明)
virtual ~Base() = default;
};
class Derived : public Base {
public:
// 重写:可以写 override,也可以不写
void func() override { // 推荐:加 override
cout << "Derived::func" << endl;
}
};
class Derived2 : public Base {
public:
// 不写 override 也能重写,但没有编译期检查
void func() {
cout << "Derived2::func" << endl;
}
};
override 的价值在于编译期检查。如果函数签名和基类的虚函数不一致(比如参数类型写错了、const 忘了写),编译器会直接报错,而不是默默创建一个新函数(此时就没有覆盖,而是隐藏)。
2.2 多态触发的三个条件
多态(动态绑定)的触发需要同时满足三个条件:
只要有一个条件不满足,就不会有动态绑定。
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
class Base3 {
public:
virtual void virtualFunc() { cout << "Base::virtualFunc" << endl; }
void nonVirtual() { cout << "Base::nonVirtual" << endl; }
};
class Derived3 : public Base3 {
public:
void virtualFunc() override { cout << "Derived::virtualFunc" << endl; }
void nonVirtual() { cout << "Derived::nonVirtual" << endl; }
};
void polyConditionDemo() {
cout << "\\n=== 多态的三个条件 ===" << endl;
Derived3 d;
// 条件满足:虚函数 + 引用
Base3& r = d;
r.virtualFunc(); // 输出 "Derived::virtualFunc"(动态绑定)
// 不满足条件 1:非虚函数
r.nonVirtual(); // 输出 "Base::nonVirtual"(静态绑定)
// 不满足条件 3:直接用对象调用
d.virtualFunc(); // 输出 "Derived::virtualFunc"(但这是静态绑定,因为对象类型是 Derived3)
// 通过对象调用虚函数:虽然是派生类对象,但是静态绑定到派生类版本
// (因为编译器知道 d 的静态类型就是 Derived3)
}
这里有一个容易混淆的点:直接通过 d.virtualFunc() 调用时,输出的是 Derived::virtualFunc,看起来"多态生效了",但其实不是动态绑定——编译器看到 d 的静态类型就是 Derived3,直接在编译期选定了 Derived3::virtualFunc。没有经过虚函数表,也没有任何运行期开销。
多态的关键在于"编译器不知道实际类型"——它只知道你有一个 Base3&,但不知道这个引用绑定的是 Base3 对象还是 Derived3 对象,所以必须留到运行期查表决定。
2.3 值传递会"丢失"多态
这是很多初学者会踩的坑。如果把派生类对象按值传给基类参数,会发生对象切片(object slicing)——派生类特有的部分被切掉了,只剩下基类部分,自然谈不上多态。
class BaseSlice {
public:
virtual void speak() const { cout << "Base::speak" << endl; }
virtual ~BaseSlice() = default;
};
class DerivedSlice : public BaseSlice {
public:
string extraData_ = "我有一大堆派生类特有的数据";
void speak() const override { cout << "Derived::speak" << endl; }
};
// 按值接收基类对象:会切片!
void processByValue(BaseSlice b) {
b.speak(); // 永远调 Base::speak
}
// 按引用接收:多态正常
void processByRef(const BaseSlice& b) {
b.speak(); // 动态绑定
}
// 按指针接收:多态正常
void processByPtr(const BaseSlice* b) {
if (b) b->speak();
}
void sliceDemo() {
cout << "\\n=== 对象切片 ===" << endl;
DerivedSlice d;
cout << "sizeof(DerivedSlice) = " << sizeof(DerivedSlice) << endl;
cout << "sizeof(BaseSlice) = " << sizeof(BaseSlice) << endl;
processByValue(d); // 切片:调 Base::speak
processByRef(d); // 多态:调 Derived::speak
processByPtr(&d); // 多态:调 Derived::speak
// 更隐蔽的切片:用基类对象接收派生类对象
BaseSlice base = d; // 切片!base 只有 BaseSlice 部分
base.speak(); // 调 Base::speak(静态绑定)
}
切片是一个严重的陷阱,因为它不报编译错误——BaseSlice base = d; 是合法的、常见的初始化语法,但派生类的 extraData_ 被"切掉了",speak 也不会调用到派生类版本。在涉及多态的场合,绝不要按值传递基类对象,一律用 const Base& 或 Base*。
2.4 虚函数不能是某些特殊函数
class Restrictions {
public:
// 错误 1:构造函数不能是虚函数
// virtual Restrictions() {} // 编译错误
// 原因:构造对象时,对象的动态类型还没确定
// 虚函数依赖虚函数表,而虚函数表在构造时才初始化
// 这是一个先有鸡还是先有蛋的问题
// 错误 2:静态成员函数不能是虚函数
// static virtual void func(); // 编译错误
// 原因:静态成员函数没有 this 指针,无法访问虚函数表
// 正确:析构函数可以是(也应该是)虚函数
virtual ~Restrictions() = default;
// 正确:普通成员函数可以是虚函数
virtual void normalFunc() {}
// 正确:私有成员函数也可以是虚函数(后面会讲)
private:
virtual void privateVirtual() {}
};
构造函数不能是虚函数,但析构函数应该是。 这个不对称性来自它们各自的作用:
- 构造函数负责"创建对象",创建时对象的实际类型是已知的(new Derived() 就明确是 Derived),不需要在运行期动态决定。
- 析构函数负责"销毁对象",销毁时如果通过基类指针操作,就需要动态绑定来调用正确的派生类析构(否则派生类的资源不会释放)。这就是为什么基类析构函数要声明为 virtual。
三、override 关键字(C++11)
3.1 为什么需要 override
C++11 之前,重写虚函数完全靠程序员自己"对齐签名"。如果签名稍有不同,编译器不会报错,而是默默地创建一个新的普通函数——这就是名称隐藏(下一节会详细讲)。
class OldStyleBase {
public:
virtual void func(int x) { cout << "Base::func(int)" << endl; }
};
class OldStyleDerived : public OldStyleBase {
public:
// 想重写,但参数写成了 double
// 结果是:没有重写,而是隐藏了基类的 func
void func(double x) { cout << "Derived::func(double)" << endl; }
};
void oldStyleProblem() {
cout << "\\n=== 没有 override 的隐患 ===" << endl;
OldStyleDerived d;
OldStyleBase& ref = d;
ref.func(10); // 调 Base::func(int)(不是重写,所以静态绑定)
// 更隐蔽的场景:const 忘了写
class Base2 {
public:
virtual void f() const { cout << "Base2::f const" << endl; }
};
class Derived2 : public Base2 {
public:
// 想重写,但忘了 const
void f() { cout << "Derived2::f(没有 const)" << endl; }
// 这不是重写!是一个新函数。基类的 const 版本没有被覆盖。
};
Derived2 d2;
Base2& ref2 = d2;
ref2.f(); // 调 Base2::f const,不是你以为的 Derived2::f
}
这类 bug 非常隐蔽:代码能编译、能运行、行为"看起来正常",但实际多态根本没生效。加 override 后,编译器会检查"这个函数确实重写了基类的某个虚函数",如果签名不对,直接报错。
3.2 override 的检查机制
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
class GoodBase {
public:
virtual void f1(int x) {}
virtual void f2(int x) const {}
virtual void f3(int x) {}
virtual ~GoodBase() = default;
};
class GoodDerived : public GoodBase {
public:
// 正确的重写:签名完全匹配
void f1(int x) override {}
void f2(int x) const override {}
// void f3(double x) override {} // 编译错误!签名不匹配
// void f3(int x) override; // 编译错误!可能因为没有 const 或非虚
};
override 的检查维度:
- 基类有没有同名虚函数?
- 参数类型是否完全一致?
- const / volatile 是否一致?
- 返回类型是否兼容(相同,或协变)?
- 引用修饰符(& / &&)是否一致?
任何一条不满足,编译器会报错。这就是 override 的核心价值:把"以为重写了其实没有"的 bug 从运行期提前到编译期。
实战建议:所有重写虚函数的地方都加 override。 这已经是 C++ 社区的共识,连 Google C++ Style Guide 都明确要求。
3.3 override 与重载、隐藏的区别
这是 C++ 里最容易混淆的三个概念,必须分清:
| 重载 | overload | 同一作用域,同名不同参 | 同一个类里 void f() 和 void f(int) |
| 隐藏 | hide | 派生类同名成员"遮盖"基类同名成员 | 派生类 void f() 隐藏基类所有 f |
| 覆盖 | override | 派生类重写基类虚函数,形成多态 | 派生类 void f() override 基类 virtual void f() |
class BaseConcept {
public:
virtual void f(int x) { cout << "Base::f(int)" << endl; }
void g() { cout << "Base::g" << endl; }
};
class DerivedConcept : public BaseConcept {
public:
// 覆盖(override):重写虚函数
void f(int x) override { cout << "Derived::f(int)" << endl; }
// 重载(overload):在派生类内部定义新的重载
void f(double x) { cout << "Derived::f(double)" << endl; }
// 隐藏(hide):同名但签名不同,隐藏基类所有 g
void g() { cout << "Derived::g" << endl; }
// void g(int x) { … } // 这也算"隐藏",而不是重载
// 因为基类 g 没有参数,派生类的 g(int) 会隐藏基类所有 g
};
void threeConceptsDemo() {
cout << "\\n=== 重载、隐藏、覆盖 ===" << endl;
DerivedConcept d;
BaseConcept& ref = d;
// 通过派生类对象调用:派生类的所有版本可见
d.f(10); // Derived::f(int)
d.f(3.14); // Derived::f(double)
d.g(); // Derived::g
// 通过基类引用调用:动态绑定
ref.f(10); // Derived::f(int)(覆盖:多态生效)
// ref.f(3.14); // 编译错误:基类没有 f(double)
ref.g(); // Base::g(g 不是虚函数,静态绑定)
}
理解这三者的区别后,很多"为什么调不到某个版本"的疑惑就迎刃而解了。override 只用于"覆盖"这个场景,重载和隐藏都不能用 override——用 override 就是告诉编译器"我这次确实是在重写基类的虚函数",如果编译器发现不是,就报错。
四、final 关键字
4.1 final 修饰虚函数
final 有两个用途:阻止某个虚函数被进一步重写,或者阻止某个类被继承。
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
class BaseFinal {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
virtual ~BaseFinal() = default;
};
class DerivedFinal : public BaseFinal {
public:
// func1 可以被继续重写
void func1() override { cout << "Derived::func1" << endl; }
// func2 是 final:之后派生类不能再重写
void func2() final { cout << "Derived::func2 (final)" << endl; }
};
class FurtherDerived : public DerivedFinal {
public:
void func1() override { cout << "FurtherDerived::func1" << endl; }
// void func2() override {} // 编译错误!func2 是 final
};
void finalFunctionDemo() {
cout << "\\n=== final 修饰虚函数 ===" << endl;
FurtherDerived fd;
BaseFinal& ref = fd;
ref.func1(); // FurtherDerived::func1(动态绑定到最终重写)
ref.func2(); // Derived::func2(final,不能再重写)
}
4.2 final 修饰类
final 修饰类时,表示这个类不能作为基类被继承。
class NotDerivable final {
public:
void func() { cout << "NotDerivable::func" << endl; }
};
// class TryDerive : public NotDerivable {}; // 编译错误!final 类不能继承
void finalClassDemo() {
cout << "\\n=== final 修饰类 ===" << endl;
NotDerivable obj;
obj.func();
// final 类的典型用途:
// 1. 工具类,本来就不打算被继承(如 std::string、std::vector 之前没标)
// 2. 性能优化:编译器知道 final 类不会有派生类,可以做更激进的优化
// 3. 库设计:明确"不要继承这个类",防止用户误用
}
4.3 final 的性能含义
final 不只是"禁止",还可能带来性能收益。如果编译器知道某个虚函数调用不会被进一步重写,就可以做去虚化(devirtualization)——直接把虚函数调用变成普通函数调用,消除运行期的 vtable 查表开销。
class Base {
public:
virtual void func() { cout << "Base" << endl; }
};
class Derived final : public Base {
public:
void func() override { cout << "Derived" << endl; }
};
void useFinalForPerformance() {
// 编译器知道 Derived 是 final,不会有 FurtherDerived
// 所以通过 Derived* 或 Derived& 调用虚函数时,可以静态绑定
Derived d;
Derived* p = &d;
p->func(); // 可能被优化成直接调用 Derived::func
// 注意:通过 Base* 调用仍然无法完全去虚化(除非编译器做全局分析)
Base* bp = &d;
bp->func(); // 仍然走 vtable
}
去虚化是一个非常强大的优化。在性能敏感的代码里,final 有时能把虚函数调用的开销直接降到零。当然,是否真的去虚化取决于编译器的具体实现和优化级别,不能依赖它做设计决策——final 的主要价值仍然是语义清晰。
4.4 final 和 override 能一起用吗
可以,而且有实际用途:
class Mid : public Base {
public:
// 重写并 final:表示"我重写这个函数,并且不允许后续重写"
void func() override final { cout << "Mid::func" << endl; }
};
这告诉后续开发者:“这个函数在这里被重写后,就不再变化了”。在框架、基类的设计中很常见——既想覆盖基类默认实现,又要给后续派生类设个底线。
五、虚函数与重载、隐藏、覆盖(深入)
前面已经简单对比了三个概念,这一节再深入看看它们的组合场景,因为这是最容易出 bug 的地方。
5.1 派生类函数隐藏基类所有同名重载
只要派生类里定义了任何同名成员(函数或变量),基类里所有同名成员都会被隐藏,不管签名是否相同。
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
class BaseHide2 {
public:
void foo() { cout << "Base::foo()" << endl; }
void foo(int x) { cout << "Base::foo(int): " << x << endl; }
void foo(double x) { cout << "Base::foo(double): " << x << endl; }
};
class DerivedHide2 : public BaseHide2 {
public:
// 只定义了无参版本
void foo() { cout << "Derived::foo()" << endl; }
};
void hidingRulesDemo() {
cout << "\\n=== 名称隐藏规则 ===" << endl;
DerivedHide2 d;
d.foo(); // OK:Derived::foo()
// d.foo(10); // 编译错误!Base::foo(int) 被隐藏了
// d.foo(3.14); // 编译错误!Base::foo(double) 被隐藏了
// 通过基类访问:正常
BaseHide2& ref = d;
ref.foo(); // Base::foo()(不是虚函数,静态绑定)
ref.foo(10); // Base::foo(int)
ref.foo(3.14); // Base::foo(double)
// 通过作用域解析访问:正常
d.BaseHide2::foo(10);
d.BaseHide2::foo(3.14);
// 用 using 声明恢复所有重载
class DerivedFix : public BaseHide2 {
public:
using BaseHide2::foo; // 把基类所有 foo 引入
void foo() { cout << "DerivedFix::foo()" << endl; }
};
DerivedFix df;
df.foo(); // DerivedFix::foo()
df.foo(10); // Base::foo(int)
df.foo(3.14); // Base::foo(double)
}
这个规则的动机:C++ 标准认为"派生类里定义了某个名字,就说明派生类要接管这个名字",如果派生类只想加一个重载而不想影响基类的所有版本,就应该显式 using。这是一种"防御性设计",虽然在很多场景下反直觉。
5.2 虚函数 + 隐藏的组合陷阱
把虚函数、隐藏、override 放在一起,会出现一些非常隐蔽的 bug:
class BaseTricky {
public:
virtual void func(int x) { cout << "Base::func(int): " << x << endl; }
virtual void func(double x) { cout << "Base::func(double): " << x << endl; }
virtual ~BaseTricky() = default;
};
class DerivedTricky : public BaseTricky {
public:
// 只重写 int 版本
void func(int x) override { cout << "Derived::func(int): " << x << endl; }
// 注意:没有重写 double 版本。
// 但 func(int) 的存在会隐藏基类所有同名函数,
// 包括基类的 func(double)!
};
void trickyHidingDemo() {
cout << "\\n=== 虚函数 + 隐藏的陷阱 ===" << endl;
DerivedTricky d;
BaseTricky& ref = d;
// 通过派生类对象调用
d.func(10); // Derived::func(int)
// d.func(3.14); // 编译错误!基类 func(double) 被隐藏了
// 通过基类引用调用
ref.func(10); // Derived::func(int)(多态)
ref.func(3.14); // Base::func(double)(多态,但派生类没重写)
// 问题:如果只想让用户从基类接口访问,行为是一致的
// 但如果有代码直接操作 DerivedTricky 对象,就会遇到"调不到 double 版本"
// 解决方案:加 using
class DerivedFixed : public BaseTricky {
public:
using BaseTricky::func;
void func(int x) override { cout << "DerivedFixed::func(int): " << x << endl; }
};
DerivedFixed df;
df.func(10); // DerivedFixed::func(int)
df.func(3.14); // Base::func(double)(通过 using 恢复)
}
这个陷阱的恶劣之处在于:它不报错、不警告、行为完全取决于你从哪个类型访问。如果代码里既有 DerivedTricky d; d.func(3.14); 又有 BaseTricky& ref = d; ref.func(3.14);,两行代码调的是不同的函数——后者能跑,前者编译错误。这类 bug 排查起来非常痛苦。
防范办法:
六、虚函数的返回值
6.1 返回值类型必须兼容
虚函数重写时,返回值类型必须与基类完全相同,或者满足协变返回(covariant return)。
class BaseReturn {
public:
virtual int getInt() { return 1; }
virtual BaseReturn* clone() { return new BaseReturn(*this); }
virtual ~BaseReturn() = default;
};
class DerivedReturn : public BaseReturn {
public:
int getInt() override { return 2; } // OK:返回值类型相同
// 协变返回:返回类型可以是基类返回类型的派生类指针/引用
DerivedReturn* clone() override { return new DerivedReturn(*this); }
// 错误:返回值类型完全不同
// double getInt() override; // 编译错误!
};
6.2 协变返回类型
协变返回(covariant return type) 是 C++ 允许的一个特例:如果基类虚函数返回 Base* 或 Base&,派生类重写时可以返回 Derived* 或 Derived&(Derived 是 Base 的派生类)。
class BaseCovariant {
public:
virtual BaseCovariant* clone() const {
return new BaseCovariant(*this);
}
virtual void print() const { cout << "Base" << endl; }
virtual ~BaseCovariant() = default;
};
class DerivedCovariant : public BaseCovariant {
public:
// 协变:返回 DerivedCovariant* 而不是 BaseCovariant*
DerivedCovariant* clone() const override {
return new DerivedCovariant(*this);
}
void print() const override { cout << "Derived" << endl; }
};
void covariantReturnDemo() {
cout << "\\n=== 协变返回 ===" << endl;
DerivedCovariant d;
DerivedCovariant* p = d.clone(); // 直接拿到 DerivedCovariant*
p->print(); // "Derived"
delete p;
// 协变的实际价值:通过派生类指针调用 clone 时,
// 直接拿到正确类型,不需要 dynamic_cast
}
协变返回的常见用途就是 clone() 函数——工厂/原型模式中,派生类的 clone 返回派生类指针,让调用者少做一次类型转换。
七、虚函数的常见误区与特殊场景
7.1 私有虚函数
虚函数可以是私有的,而且这并不会阻止多态——多态是运行期行为,访问控制是编译期检查,两者独立。
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
class PrivateVirtualBase {
public:
// 公开接口调用私有虚函数(模板方法模式)
void templateMethod() {
doWork(); // 调用私有虚函数,多态生效
}
virtual ~PrivateVirtualBase() = default;
private:
// 私有虚函数:派生类可以重写
virtual void doWork() { cout << "Base::doWork" << endl; }
};
class PrivateVirtualDerived : public PrivateVirtualBase {
private:
void doWork() override { cout << "Derived::doWork" << endl; }
};
void privateVirtualDemo() {
cout << "\\n=== 私有虚函数 ===" << endl;
PrivateVirtualDerived d;
d.templateMethod(); // 输出 "Derived::doWork"
// 即使 doWork 是私有的,多态依然生效
// 因为访问控制是编译期的,多态是运行期的
// 外部不能直接调用 d.doWork(),但通过基类公开接口可以
// 这是模板方法模式的经典实现
}
私有虚函数 + 公有非虚接口是模板方法模式(Template Method Pattern) 的经典实现:基类提供稳定的公开接口,把可定制的部分设计为私有虚函数,派生类重写它。这样派生类只能改"变化的部分",不能改"稳定的接口"——这就是"依赖倒置"和"开闭原则"的具体落地。
7.2 虚函数与 inline
inline 和 virtual 并不冲突,但效果受限:
class InlineVirtual {
public:
// 虚函数可以 inline(在类内定义)
virtual void func() { cout << "InlineVirtual::func" << endl; }
};
为什么说效果受限?
- 通过 Base* 调用虚函数,需要查 vtable,无法 inline(编译器不知道具体是哪个版本)
- 通过 Derived 对象直接调用,编译器可以静态绑定到 Derived::func,此时可以 inline
- 如果编译器能证明类型(比如 final 类、final 虚函数、对象在栈上的封闭使用),也可以去虚化后 inline
实践中:虚函数体内联的收益通常不大,写虚函数时不用纠结"要不要 inline"。虚函数调用本身的开销(间接跳转 + 缓存失效)往往比函数体本身还大,这一点在下一篇讲 vtable 时会详细分析。
7.3 虚函数与默认参数
虚函数可以带默认参数,但默认参数在静态绑定阶段解析,这会导致一个非常反直觉的行为:
class BaseDefault {
public:
virtual void func(int x = 10) {
cout << "Base::func(" << x << ")" << endl;
}
virtual ~BaseDefault() = default;
};
class DerivedDefault : public BaseDefault {
public:
void func(int x = 20) override {
cout << "Derived::func(" << x << ")" << endl;
}
};
void defaultParamDemo() {
cout << "\\n=== 虚函数 + 默认参数 ===" << endl;
DerivedDefault d;
BaseDefault& ref = d;
ref.func(); // 输出 "Derived::func(10)" !
// 函数体是 Derived::func(动态绑定)
// 但默认参数是 10(来自 BaseDefault,静态绑定)
d.func(); // 输出 "Derived::func(20)"(静态绑定到 Derived)
}
规则:虚函数的默认参数取自静态类型,函数体取自动态类型。这几乎是一个"反模式"——两套规则混在一起,非常容易出错。
实战建议:虚函数不要用默认参数。 要么把默认值放在无参重载里,要么把参数显式传。Google C++ Style Guide 也明确禁止虚函数使用默认参数。
7.4 虚函数与 static_cast / dynamic_cast
class BaseCast {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual ~BaseCast() = default;
};
class DerivedCast : public BaseCast {
public:
void f() override { cout << "Derived::f" << endl; }
void derivedOnly() { cout << "只有派生类有" << endl; }
};
void castVirtualDemo() {
cout << "\\n=== 虚函数与类型转换 ===" << endl;
DerivedCast d;
BaseCast* bp = &d;
// 静态转换:不做运行期检查,直接转换
DerivedCast* dp1 = static_cast<DerivedCast*>(bp);
dp1->derivedOnly(); // 安全,因为 bp 确实指向 DerivedCast
// 如果 bp 实际指向 BaseCast 对象?
BaseCast base;
BaseCast* bp2 = &base;
// DerivedCast* dp2 = static_cast<DerivedCast*>(bp2);
// dp2->derivedOnly(); // UB!运行时可能崩溃
// 动态转换:运行期检查,失败返回 nullptr
DerivedCast* dp3 = dynamic_cast<DerivedCast*>(bp2);
if (dp3 == nullptr) {
cout << "转换失败,返回 nullptr" << endl;
}
}
关键规则:
- static_cast 从基类指针转到派生类指针时不做运行期检查,开发者自己负责。如果实际对象不是派生类,是 UB。
- dynamic_cast 依赖 RTTI(运行时类型信息),需要基类有虚函数(至少有一个,通常是虚析构)。这是 dynamic_cast 能工作的前提。RTTI 和 dynamic_cast 会在后续专门一篇讲。
八、综合实战:一个基础图形绘制系统
代码如下(示例):
/*———————————–【程序说明】—————————-
* 项目命题: C++基础宝典
* 代码所属: 瑶池酒剑仙(枫之剑客)
* 作者: 阿甘
* 开发时间: 2026/11/06
*IDE 版 本: Visual Studio 2026
* 项目版本: 1.0.0.1
*—————————————————————————*/
//原文链接:https://ganzp1688.blog.csdn.net/?type=blog
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <cmath>
// 抽象基类:所有图形必须实现 area() 和 draw()
class Shape {
public:
virtual ~Shape() = default;
// 纯虚函数:派生类必须实现
virtual double area() const = 0;
virtual void draw() const = 0;
// 虚函数:提供默认实现,派生类可重写
virtual void printInfo() const {
std::cout << "Shape: " << name()
<< ", area = " << area() << std::endl;
}
// 非虚函数:提供稳定的公共接口
const std::string& name() const { return name_; }
protected:
std::string name_;
};
// 第一层派生
class Shape2D : public Shape {
protected:
double x_, y_;
public:
Shape2D(double x, double y) : x_(x), y_(y) {}
void move(double dx, double dy) { x_ += dx; y_ += dy; }
virtual int dimension() const { return 2; }
};
// 第二层派生:具体图形
class Circle : public Shape2D {
private:
double radius_;
public:
Circle(double r, double x = 0, double y = 0)
: Shape2D(x, y), radius_(r) {
name_ = "Circle";
}
double area() const override {
return 3.141592653589793 * radius_ * radius_;
}
void draw() const override {
std::cout << "绘制圆形(半径 " << radius_ << ")" << std::endl;
}
};
class Rectangle : public Shape2D {
protected:
double w_, h_;
public:
Rectangle(double w, double h, double x = 0, double y = 0)
: Shape2D(x, y), w_(w), h_(h) {
name_ = "Rectangle";
}
double area() const override { return w_ * h_; }
void draw() const override {
std::cout << "绘制矩形(" << w_ << "x" << h_ << ")" << std::endl;
}
};
class Square final : public Rectangle {
public:
Square(double side, double x = 0, double y = 0)
: Rectangle(side, side, x, y) {
name_ = "Square";
}
// Square 已经 final,不能再被继承
void draw() const override {
std::cout << "绘制正方形(边长 " << w_ << ")" << std::endl;
}
};
// 集合:统一管理不同图形
class Scene {
private:
std::vector<std::unique_ptr<Shape>> shapes_;
public:
void add(std::unique_ptr<Shape> s) {
shapes_.push_back(std::move(s));
}
void drawAll() const {
for (const auto& s : shapes_) {
s->draw();
}
}
void printAllInfo() const {
for (const auto& s : shapes_) {
s->printInfo();
}
}
double totalArea() const {
double sum = 0.0;
for (const auto& s : shapes_) {
sum += s->area();
}
return sum;
}
};
void comprehensiveDemo() {
std::cout << "\\n╔══════════════════════════════════════════════════════════════╗\\n";
std::cout << "║ 虚函数综合实战:图形系统 ║\\n";
std::cout << "╚══════════════════════════════════════════════════════════════╝\\n";
Scene scene;
scene.add(std::make_unique<Circle>(5.0));
scene.add(std::make_unique<Rectangle>(4.0, 6.0));
scene.add(std::make_unique<Square>(3.0));
std::cout << "\\n=== 绘制所有图形 ===" << std::endl;
scene.drawAll();
std::cout << "\\n=== 所有图形信息 ===" << std::endl;
scene.printAllInfo();
std::cout << "\\n总面积: " << scene.totalArea() << std::endl;
// 演示多态删除
std::cout << "\\n=== 多态删除(正确释放所有派生类资源) ===" << std::endl;
// 离开作用域时,unique_ptr 逐个 delete
// 因为 Shape 有虚析构,每个对象的正确析构函数会被调用
}
int main() {
noVirtualDemo();
virtualBasicDemo();
polyConditionDemo();
sliceDemo();
oldStyleProblem();
finalFunctionDemo();
finalClassDemo();
threeConceptsDemo();
hidingRulesDemo();
trickyHidingDemo();
covariantReturnDemo();
privateVirtualDemo();
defaultParamDemo();
castVirtualDemo();
comprehensiveDemo();
return 0;
}
九、总结与下一篇预告
9.1 虚函数核心结论
| 为什么需要虚函数? | 让 C++ 支持运行期多态(按实际对象类型调用) |
| 多态触发的三个条件? | 虚函数 + 派生类重写 + 通过基类指针/引用调用 |
| 值传递会发生什么? | 对象切片:派生类部分被切掉,多态失效 |
| 构造函数可以是虚函数吗? | 不能。构造时对象类型已知,不需要动态绑定 |
| 析构函数可以是虚函数吗? | 可以,而且如果基类会被多态 delete,必须 virtual |
| override 有什么用? | 编译期检查函数签名是否与基类虚函数匹配 |
| final 有什么用? | 阻止进一步重写,或阻止继承 |
| 重载、隐藏、覆盖的区别? | 重载:同作用域同名不同参;隐藏:派生类遮盖基类;覆盖:重写虚函数形成多态 |
| 重写时返回值可以不同吗? | 只有协变返回(派生类返回派生类指针/引用)可以 |
| 虚函数可以有默认参数吗? | 可以,但默认参数静态绑定,几乎总是反直觉,不要用 |
| 私有虚函数有用吗? | 有,是模板方法模式的经典实现 |
9.2 八条实战军规
9.3 练习题
练习1: 下面的代码输出什么?解释原因:
class Base {
public:
virtual void f() { cout << "Base" << endl; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
void f() { cout << "Derived" << endl; }
};
int main() {
Derived d;
Base b = d; // 注意这里不是引用
b.f();
}
练习2: 解释下面代码为什么调不到 Base::foo(double),并给出两种修复方式:
class Base {
public:
void foo(int x) { cout << "int" << endl; }
void foo(double x) { cout << "double" << endl; }
};
class Derived : public Base {
public:
void foo(int x) { cout << "derived int" << endl; }
};
int main() {
Derived d;
d.foo(3.14); // 编译错误
}
练习3: 用虚函数设计一个简单的日志系统:
- Logger 抽象基类,有 log(level, msg) 纯虚函数
- ConsoleLogger、FileLogger、NullLogger 三个派生类
- 要求通过 Logger& 调 log 能动态绑定
- 加入 override 和 final 的合理使用
练习4: 解释为什么构造函数不能是虚函数。如果非要实现"工厂"语义(根据参数创建不同派生类对象),应该怎么做?
练习5: 下面的代码中,Base 析构函数没有 virtual,会有什么问题?如何修复?为什么?
class Base {
public:
~Base() { cout << "Base destructor" << endl; }
};
class Derived : public Base {
int* data_ = new int[100];
public:
~Derived() { delete[] data_; cout << "Derived destructor" << endl; }
};
int main() {
Base* p = new Derived();
delete p;
}
9.4 下一篇预告
下一篇我们深入 虚函数表(vtable) 的底层实现:
- vtable 和 vptr 的内存布局:一个对象到底长什么样
- 虚函数调用的开销分析:间接跳转、缓存失效、去虚化
- 多重继承下的 vtable 布局:为什么会有多个 vptr
- 虚函数与对象大小的关系:虚函数让对象"胖"了多少
- 通过指针手工模拟虚函数调用:深入理解 vptr 的定位
- 编译器去虚化(devirtualization)的条件
- RTTI 与 vtable 的关系
- 综合实战:用结构体和函数指针手写一个简易多态
掌握了 vtable 的原理,你就真正理解了"多态"的代价和本质——为什么虚函数调用比普通函数慢、为什么多态会让对象变大、为什么某些场景必须避免虚函数。这对做高性能 C++ 的开发者来说是必修课。
敬请期待!

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!
🌲 请进入下一专栏: 项目实战合集
🌲 彩蛋壁纸别忘了先看哈!

新壁纸(青雀黄龙之舳)

网硕互联帮助中心



评论前必须登录!
注册