# 纲要 > 主干纲要、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