# 纲要
> 主干纲要、Hint/线索/路标
- **多态** (静态、动态多态)
- **运行时多态**
- 动态绑定
- **虚函数**
- 派生类重写虚函数
- 虚函数的默认参数
- **虚函数表与虚指针**
- 虚函数表
- 虚指针
- **纯虚函数**与 **抽象类**
%%
# Q&A
#### 已明确
[[02-开发笔记/01-cpp/类与对象/cpp-多态#多态| ❓ 什么是多态?]]
![[02-开发笔记/01-cpp/类与对象/cpp-多态#^4rlyne]]
![[02-开发笔记/01-cpp/类与对象/cpp-多态#^d95fze]]
![[02-开发笔记/01-cpp/类与对象/cpp-多态#^4qrfjr]]
[[#虚函数|❓什么是虚函数?]]
![[02-开发笔记/01-cpp/类与对象/cpp-多态#^42ol1a]]
[[#纯虚函数|❓什么是纯虚函数、什么是抽象类?]]
![[02-开发笔记/01-cpp/类与对象/cpp-多态#^je8jdt]]
#### 待明确
> 当下仍存有的疑惑
❓<font color="#c0504d">虚函数表中包含哪些项?</font>
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
<br><br>
# 多态
> ❓<font color="#c0504d">什么是多态性(Polymorphism)?</font>
多态性:指实现**相同的操作或函数**在**不同的对象上产生不同的行为**。包括两类:
- **==编译时多态==**(**静态多态**):
- **函数重载**(Function Overloading):同一个函数名可以有多个不同的实现,参数列表不同。编译器根据调用时提供的类型参数在编译时决定调用哪个函数
- **运算符重载**(Operator Overloading):自定义运算符的行为,使其适用于特定的类或类型。
- **模板**(Templates):使用模板类或函数,允许同一段代码处理不同的数据类型。
- **==运行时多态==**(**动态多态**)
- **虚函数(Virtual Functions)**:**基类中的虚函数在派生类中==可以被重写==**,**通过基类指针或引用调用时,实际调用的是派生类的实现**。
- **抽象类(Abstract Classes)**:含有**纯虚函数**的类,不能实例化,只能作为基类使用。
##### 多态的优点
1. **灵活性**:多态使得代码更具弹性和可扩展性,可以轻松地引入新的派生类而不影响现有代码。
2. **代码重用**:通过基类定义通用接口,派生类实现具体功能,代码重用性大大增强。
3. **简化代码**:使用多态,可以用统一的接口处理不同的对象,简化代码逻辑。
##### 多态的应用场景
- **设计模式**:如工厂模式、策略模式、观察者模式等,广泛利用多态性。
- **框架和库设计**:通过多态性提供灵活的扩展接口。
- **抽象接口**:定义抽象基类,派生类实现具体功能,广泛应用于驱动程序、图形界面等。
<br><br>
# 运行时多态
> 运行时多态(Runtime Polymorphism),也称为动态多态性(Dynamic Polymorphism)
> ❓ <font color="#c0504d">什么是运行时多态?</font> ^4rlyne
运行时多态:指程序在运行时能够**根据对象的==实际类型==(动态类型)** 调用对应版本的函数。
C++中的运行时多态基于 **虚函数** 和 **继承** 实现,体现为:
- **基类定义了==虚函数==**,而其 **`public` 派生类==重写==了该虚函数**;
- 当一个**基类的指针或引用** 实际上 **指向一个派生类对象**时,通过该指针或引用**调用==基类的 "虚函数"==** ,**实际执行的是==派生类重写的函数实现==**。
> [!NOTE] 「运行时多态」本质: **继承体系中 "虚函数" 在通过 "基类指针或引用" 而调用时的 "==运行时的动态绑定=="** !
> [!caution] 运行时多态只存在于 "==`public` 继承体系=="中。仅在 `public` 继承下,编译器才允许 **"派生类指针或引用" 到 "基类指针或引用"** 的向上转型。
> 参见 [[02-开发笔记/01-cpp/类型相关/cpp-类型转换#向上转型的可访问性 🚨|cpp-类型转换#向上转型的可访问性]] ^pe7ujd
> [!caution] "==基类指针或引用==" 只能访问派生类对象中的 "**==基类部分==**"
> **基类指针(或引用)指向派生类对象**时,通过该指针/引用 **只能访问==基类中定义的成员==(包括在派生类中重写的基类虚函数)**,而**不能访问==派生类特有的成员==**。
> 原因在于,编译时编译器只能根据**指针的静态类型**来确定可以访问哪些成员。 ^je8jdt
#### **运行时多态**示例
> 示例可见[^5]
```cpp title:virtual_func_exam.cpp
struct BaseClass {
virtual void VirtualFunc() const { // 基类的虚函数
cout << "This is virtual func in base class\n";
}
virtual ~BaseClass() = default;
};
struct Derived1 : public BaseClass {
void VirtualFunc() const override { // 派生类显式重写的虚函数. "override"说明符
cout << "This is virtual func override by derived class 1.\n";
}
};
struct Derived2 : public BaseClass {
void VirtualFunc() const override { // 派生类显式重写的虚函数. "override"说明符
cout << "This is virtual func override by derived class 2.\n";
}
};
// 该函数接受基类指针, 调用对象的"虚函数", 能够支持运行时多态
void PolymorphicFunction(const BaseClass& obj) {
obj.VirtualFunc();
}
int main() {
BaseClass* ptr_base;
Derived1 obj1;
Derived2 obj2;
ptr_base = &obj1; // 基类的指针, 实际指向其派生类Derived1的实例对象.
ptr_base->VirtualFunc(); // 调用`Derived1`中重写的虚函数;
ptr_base = &obj2; // 基类的指针, 实际指向其派生类Derived2的实例对象.
ptr_base->VirtualFunc(); // 调用`Derived2`中重写的虚函数;
// `PolymorphicFunction`接收基类指针
PolymorphicFunction(obj1); // `Derived1`中重写的虚函数;
PolymorphicFunction(obj2); // `Derived2`中重写的虚函数;
}
```
<br><br><br>
# 动态绑定
> ❓<font color="#c0504d">运行时多态背后的实现机制是什么?</font> ^4qrfjr
> **动态绑定**(dynamic biding),也称 "**运行时绑定**" ,是实现**运行时多态**的关键机制。
「**动态绑定机制**」:当通过 **==基类指针或引用==** 调用 **==基类声明的 "虚函数" ==** 时,将在==**运行时**==根据**指针或引用==所指向对象的实际类型==** 来执行**对应的函数版本**[^2] [^4]。
> [!NOTE] "**引用或指针**的**静态类型与动态类型可以不同**" 正是 C++支持多态性的根本所在。
>
> C++中支持 "**派生类到基类**" 的隐式类型转换,因而可以**将 "==基类的指针或引用==" 绑定到 "==派生类对象=="**。
> 在此基础上,结合对 "虚函数" 的**动态绑定**机制,即可实现运行时多态。
> [!NOTE] "对非虚函数的调用" 以及 "通过对象进行的函数调用(虚函数或非虚函数)",都在 "编译时" 绑定。
![[_attachment/02-开发笔记/01-cpp/类与对象/cpp-多态.assets/IMG-cpp-多态-3883F2A761351CBD114ABCEF88B2C077.png|565]]
<br>
## 动态绑定的实现原理
动态绑定的实现依赖于 **==虚函数表==(vtable)**,当**调用一个虚函数**时,会查找对象的虚函数表,以确定应该实际执行哪个函数版本。
<br>
## 取消动态绑定
如果需要取消动态绑定,可通过**作用域运算符 `::`** 明确指定**调用==基类的虚函数版本==**。
> [!caution] 必须取消动态绑定的场景
>
> 通常,只有**在派生类本身重写的虚函数版本内,需要调用==基类的虚函数版本==时**,才会需要取消动态绑定。
> 在该情况下,如果不明确指定调用基类的虚函数,则将**被编译器解析为对==派生类版本的调用==**,从而导致无限递归。
<br>
## 动态绑定与 RTTI 的区别
C++中的动态绑定和运行时类型识别(RTTI)是**相关但不完全相同的两个概念**,都与**多态性和类型信息**有关。
- **动态绑定**:用于 **==实现多态性==**——通过基类指针或引用来实际指向其派生类对象,从而调用派生类对象重写的虚函数版本。
- **运行时类型识别**(RTTI):用于在 **==运行时查询和操作==对象的类型信息**。
- RTTI 主要通过两个操作符实现:`dynamic_cast` 和 `typeid`。
<br><br><br>
# 虚函数表与虚指针
> - **虚函数表**(Virtual Table,vtbl)
> - **虚指针**(Virtual Pointer,vptr)
![[02-开发笔记/01-cpp/类与对象/cpp-多态#^1oylf0]]
<br>
## 虚函数表
「虚函数表」是一个在编译时生成的 **函数指针数组**。
每个具有 "**虚函数**" 的 **类** 都持有一个 "**==虚函数表==**",存放了**该类的所有虚函数的地址**,表中元素为 **指向该类的虚函数实现的==函数指针==**。
虚函数表中包括:
- class 所关联的 `type_info` 对象(用于实现 RTTI);通常位于**表格中的第一个 slot**
- 指向类中各个虚函数的**函数指针**
![[_attachment/02-开发笔记/01-cpp/类与对象/cpp-多态.assets/IMG-cpp-多态-050FCA014B547CB0FC55A0368EA8D4DF.png|726]]
<br>
## 虚指针
每个具有 "**虚函数**" 的类的**实例对象** 都持有一个 "**==虚指针==** `vptr`"——**==指向其所属类的虚函数表==**。
- **vptr 的初始化**:当对象被创建时,编译器会自动为其加入一个虚指针,并初始化为**指向该对象所属类的虚函数表**。
- **vptr 的作用**:通过虚指针,程序在运行时可以**根据对象的实际类型找到对应的虚函数表**,并**从中获取正确的虚函数地址,实现多态性**。
<br><br>
# 虚函数
「**虚函数**」:在基类中**通过==关键字 `virtual` 声明==的成员函数**,可**在派生类中被"重写"**。
虚函数用于实现**运行时多态**——当通过 **==基类的指针或引用==** 来调用**基类的虚函数**时,实际调用的是**派生类中的==重写版本==**。
> [!caution] 只有 =="**虚函数**"== 才能被重写,只有 "==**虚函数**==" 才具有运行时多态的特性。
>
> 说明示例:
>
> ```cpp
> struct Base {
> void mem_func() { cout << "Base::mem_func()" << endl; }
> virtual void vir_func() { cout << "Base::vir_func()" << endl; };
> };
>
> struct Derived: public Base {
> // "隐藏"基类的同名函数.
> void mem_func() { cout << "Derived::mem_func()" << endl; }
> // "重写"基类的"虚函数"
> void vir_func() override { cout << "Dervied::vir_func()" << endl; }
> };
>
> int main() {
> Derived obj;
> Base* ptr = &obj;
> ptr->mem_func(); // 对于非虚函数, 通过基类指针调用的是 "Base::mem_func()"
> ptr->vir_func(); // 对于虚函数, 通过基类指针调用的是 "Derived::vir_func()"
> return 0;
> }
> ```
>
> [!NOTE] `virtual` 关键字仅在类定义内部声明成员函数时使用,类外部的函数定义不能带有该关键字。
> [!NOTE] 虚函数的传递性
>
> 基类中声明的**虚函数**,**在其所有派生类中也都是虚函数**(隐式地,重写时无需再使用 `virtual` 声明) 。
> 因此**派生类同样将具有自己==独立的虚函数表==**。
> ^1oylf0
> [!faq] ❓<font color="#c0504d">什么成员函数可以声明为 "虚函数"? </font>
>
> 任何**构造函数之外的非静态成员函数**都可以作为虚函数。 ^42ol1a
> [!NOTE] **必须为每一个虚函数提供** "定义",无论其是否被使用[^3]
>
> - 对于声明的**常规函数**而言,如果不使用该函数,则可以 "**仅声明而不提供定义**";
> - 对于声明的**虚函数**,无论是否使用,**==必须提供定义==**,否则编译错误。
> - 由于动态绑定机制,**直到运行时才能确定实际执行的虚函数版本**,因此编译器需要确保所有虚函数都存在定义。
>
> Ps:所以基类的虚析构函数会声明为 `= default`。
> [!caution] 在 **==基类==** 的 "**==构造/析构函数==**" 中调用的虚函数始终是 "**==基类==**" 的版本!
> 参见 [[77-阅读/CS 相关/《Effective C++》#Item9 绝不在构造和析构过程中调用 virtual 函数|《Effective C++》#Item9]]。而在 "**派生类**" 的构造/析构函数中,**直接调用的虚函数**(未指定基类作用域`::`)始终是 "**派生类**" 的版本。
<br><br>
## 派生类重写虚函数
派生类可以为其 **"重写" 的虚函数**在形参列表后(const、引用限定符之后)**显式注明 "`override`" 关键字**,指示编译器进行编译时检查。
> [!note] **派生类如果没有重写虚函数**,则**将直接继承基类中的版本**。
> [!NOTE] 对虚函数的重写要求
>
> 当**派生类**覆盖/重写 (override)某个虚函数时,其 **==重写版本的形参、返回类型==必须与==基类的形参、返回类型==严格匹配**。
> 否则,编译器将认为新定义的函数是一个派生类特有的 "独立" 函数,而非对基类虚函数的 "覆盖"。
>
>
> 注:若**基类虚函数的==返回类型==** 是 **==基类本身的指针或引用==**,则**派生类覆写的虚函数的返回类型**可以是 **==派生类的指针或引用==**。
> (形参则不行)
>
#### override 说明符
参见: [[02-开发笔记/01-cpp/类与对象/cpp-类成员函数的基本说明#override 说明符|cpp-类成员函数的基本说明#override 说明符]]
<br><br>
## 虚函数的默认实参
虚函数可以有**默认实参**。如果虚函数使用了默认实参,则**最好保证==基类和派生类的默认实参一致==**。
> [!caution] 动态绑定机制下,将使用**==基类的默认实参==**!
>
> 在动态绑定机制下,当**通过==基类的指针或引用==来调用虚函数时**,即使实际调用的是派生类重写的版本,也**将使用==基类中定义的默认实参==**。
>
<br><br><br>
# 多态类
> ❓ <font color="#c0504d">什么是多态类?</font> ^d95fze
**多态类(polymorphic)** :能够表现出**动态多态性**的类,即 "**==具有虚函数的基类及其派生类==**":
- **虚函数**:多态类至少包含一个虚函数
- **基类和派生类**:基类定义了一个或多个虚函数,派生类重写这些虚函数来实现运行时多态。
**==多态类==至少包含一个==虚函数==**,可在运行时展现**动态多态性**。
> [!caution] "**多态基类**" 应当定义一个 "**==虚析构函数==**"
>
> 多态基类应当声明其**析构函数为虚函数**,
> 从而**保证通过基类指针或引用==删除派生类对象==时,会正确调用==派生类的析构函数==**,而不会错误地调用基类的析构函数[^1]。
>
> 如果多态基类的析构函数不是虚函数,则 `delete` 一个**指向派生类对象的基类指针** 将调用基类的析构函数,这是未定义行为。
>
> ```cpp
> class MyClass { // 多态基类
> virtual void func();
> virtual ~MyClass() = default; // 声明&定义虚析构函数
> };
> ```
> ^en75lb
<br><br>
# 纯虚函数
「**纯虚函数**」:**函数声明中标识为 `= 0`的虚函数**,表明该虚函数**仅定义了一个接口**,**必须==由派生类提供具体的实现==**。
```cpp title:abstract_class.cpp
class AbstractBase { // 抽象类, 该类不能被实例化.
public:
virtual void pureVirtualFunc() = 0; // 纯虚函数
};
```
> [!NOTE] **纯虚函数无需定义**,但也可为其 "在类外" 提供函数体实现(尽管没有意义),是可编译的。
> [!caution] 当基类的 "析构函数" 被声明为 "纯虚函数" 时,**必须显式定义**。
>
> 否则派生类将无法析构(**编译器无法为其调用基类析构函数**,将报告链接错误: "*undefined reference to ...*")
>
> ```cpp
> class AbstractBase {
> public:
> virtual ~AbstractBase() = 0; // 纯虚析构函数, 故编译器不会为其自动生成合成版本.
> };
>
> // 必须手动定义, 使基类的纯虚析构函数的存在, 否则派生类析构时将找不到基类的析构函数, 报告"链接错误".
> AbstractBase::~AbstractBase() {};
> ```
>
>
<br><br>
# 抽象类
「**抽象类**」:具有**纯虚函数**的类称为 **==抽象基类==**(abstruct base class)
**抽象类==不能被实例化==,只能用作基类,用于为派生类==定义统一的 "接口"==,由==派生类==来具体实现接口**。
> [!caution] 如果一个**派生类没有对继承而来的==纯虚函数==定义自己的版本**,则**该派生类也是抽象类**。(故不能被实例化)
<br><br><br>
# 在 STL 容器中存储多态性对象
> [!NOTE]
> 由于**容器中存放的元素**必须是**同一静态类型**,因此当涉及到基类和派生类对象时,**直接存储派生类对象将会导致==对象切片(Object Slicing)==问题**——派生类的部分将被 "切掉" 而只保留基类的部分,**派生类特有的数据成员和方法将丢失**。
>
当需要使用容器存放和管理**继承体系中的对象**时,必须通过**间接存储**的方式,存放**基类的指针**:
- 使用**原始指针**(不推荐,需要手动管理内存)
- 使用**智能指针**(推荐,能够自动管理内存)
使用容器**存储==指向基类的指针==能够保持对象的多态性**,可以通过基类指针调用到派生类的方法。
**智能指针(特别是 `std::shared_ptr`)是管理具有继承关系的对象生命周期的推荐方式**,因其能够自动处理对象的创建和销毁,避免了内存泄漏的风险。
使用示例:
```cpp title:store_polymorphic_class_in_container.cpp
class Base {
public:
virtual void Func() const {
cout << "Base class" << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
void Func() const override {
cout << "Derived class" << endl;
}
};
int main() {
// vector中的元素类型为基类的智能指针
vector<std::shared_ptr<Base>> my_vec;
my_vec.push_back(std::make_shared<Base>()); // 存入指向基类对象的智能指针
my_vec.push_back(std::make_shared<Derived>()); // 存入指向派生类对象的智能指针
for (const auto& ptr : my_vec) {
ptr->Func(); // 多态调用
}
}
```
<br><br>
# 参考资料
# Footnotes
[^1]: 《Effective C++》Item 7
[^2]: 《C++ Primer》P527
[^3]: 《C++ Primer》P536
[^4]: 《C++ Primer》P537
[^5]: 《深度探索 C++对象模型》P24-27