多态
在C++中,多态是面向对象编程的一个重要特性,它允许不同的对象对同一消息做出不同的响应。
[TOC]
多态性可以提高代码的可扩展性和可维护性,主要分为静态多态和动态多态。
静态多态:模板、函数重载
动态多态:虚函数、继承
静态多态
静态多态也称为编译时多态,它在编译阶段就确定了要调用的函数。主要通过模板和函数重载来实现。
模板与泛型编程
模板是C++中实现泛型编程的重要工具,它允许编写与类型无关的代码。模板分为函数模板和类模板。
泛型函数的参数由关键字 requires 定义。
使用模板提升代码抽象层级
正确使用 requires 关键字避免过度约束。
泛型编程的目标是针对具有相似语义属性的类型集合,高效地泛化操作/算法。
算法约束:算法必须通过概念
requires明确约束模板参数的要求,避免直接使用运算符而不检查类型支持性。
template<typename T>
requires Arithmetic<T> // 明确约束T必须支持算术运算
T sum(vector<T>& v, T s) {
for (auto x : v) s += x;
return s;
}概念构建:允许定义简单概念作为构建块,但算法应使用足够强的概念来确保类型安全性。
使用模板表达适用于多种参数类型的算法
STL 的基础就是使用模板来适用不同参数类型的算法实现。
除非确实需要支持多种模板参数类型,否则不要使用模板。避免过度抽象。
使用模板来表达容器元素类型
容器需要元素类型,将其作为模板参数表达具有通用性、可复用性和类型安全性。
C 语言在容器中通常使用 void* 来保存不同元素类型,并通过设置元素第一个字节为参数类型进行区分。
静态多态和动态多态的互补
使用静态多态实现动态多态:
提供通用、便捷的静态绑定接口,但内部进行动态分发,从而实现统一的对象布局。例如std::shared_ptr的删除器所采用的类型擦除技术(但需避免过度使用类型擦除)。
类型擦除通过在独立编译边界后隐藏类型信息,会引入额外的间接层开销。
函数重载
同一作用域内,可以定义多个同名但参数列表不同的函数,编译器根据调用的实参类型和数量选择合适的函数。
优先使用默认参数而非函数重载
默认参数仅为单一实现提供替代接口,而一组重载函数无法保证始终实现相同的语义。使用默认参数可避免代码重复。
当一组函数用于对不同类型执行语义等价的操作时,不存在这种选择。
动态多态
动态多态也称为运行时多态,它在运行时才确定要调用的函数。主要通过虚继承和虚函数来实现。
虚函数
虚函数是实现多态的核心机制,允许通过基类指针或引用调用子类中重写的方法,从而在运行时决定实际调用哪个函数,也称为动态绑定(runtime dispatch)。
当一个类中含有虚函数时:
编译器为它创建一张虚函数表(vtable);
每个对象内部会有一个隐藏的虚表指针(vptr)指向对应的虚表;
当调用虚函数时,运行时根据 vptr 找到虚表,再找到对应函数指针并调用它。
纯虚函数
纯虚函数在虚函数声明后添加 =0,强制派生类提供自己的实现。
核心作用:
定义接口规范,强制派生类实现特定方法。
使基类称为抽象类,无法实例化对象,只能作为接口被继承。
常见问题
构造函数能否是虚函数?
不能。
(1)虚函数调用依赖于对象的完整性
虚函数需要通过对象的虚表指针 vptr 调用,但在构造函数执行过程中,对象还不是完整的子类实例,虚表指针 vptr 还未初始化指向子类的虚表。所以构造函数执行期间无法实现动态绑定。
(2)构造函数的调用方式是静态的
构造函数无法通过基类指针或引用间接调用,不符合虚函数的动态绑定语义。
析构函数能否是虚函数?
基类指针指向派生类对象时,基类析构函数应当是虚函数。
如果没有将基类析构函数声明 virtual ,析构函数调用会采用静态绑定,通过基类指针删除派生类对象时,编译器仅调用基类的析构函数,导致派生类资源无法释放。
将基类析构函数声明为虚函数,确保删除基类指针时,先调用派生类析构函数,再调用基类析构函数。
虚析构函数使对象的虚表包含析构函数的地址,删除基类指针时,通过虚表找到实际对象类型的析构函数并调用。
虚函数可以用默认参数吗?
虚函数可以有默认参数,但是默认参数的选择是在编译器静态绑定的,虚函数的调用属于动态绑定。
编译器在编译期间通过指针类型来确定默认参数值。
编译器遇到函数的处理机制:
参数解析(编译期间):确定函数签名;解析默认参数值;进行类型检查和转换。
函数调用(运行期间):通过虚函数表(vtable)确定实际调用的函数;执行响应的函数体。
在虚函数中使用默认参数可能会产生误导,应该避免使用。
可以通过函数重载、模板和策略模式来替代默认参数。
相关问题:
虚析构函数也可以有默认参数,但是没有人会这样使用。
纯虚函数可以有默认参数,结果同虚函数。
多重继承中的复杂情况,具体要看调用的指针类型。
能否使用数组保存多态虚基类?
使用数组保存存在虚函数的类是非常危险的操作。
数组是一块连续的内存,而虚基类和继承类的内存如下:
两个类的内存大小并不相同,如果用数组保存 Base 类,可能会导致访问错误的内存地址引发段错误。
替代方法:
指针数组:数组保存基类指针。
智能指针容器:
std::vector<std::uniquee_ptr<Base>>现代 C++ 特性:
std::variant、concepts 等。
虚继承
虚继承(Virtual Inheritance) 是一种用于解决菱形继承缺陷的机制。
多继承
一个派生类可以同时继承多个基类。
命名冲突:多个基类有同名的成员(变量或者方法),派生类无法直接调用该成员,需要明确基类成员。
设计复杂:违背单一职责原则,代码可读性、维护性下降。
构造、析构顺序问题:多继承基类的构造函数按继承声明顺序执行,机构函数按相反顺序执行。逻辑容易出错导致资源泄露。
菱形继承
菱形继承时多继承的特殊情况,两个中间基类同时继承顶层基类,底层派生类又同时继承两个中间基类。
数据冗余:顶层基类的成员在底层派生类中存在多份副本,浪费内存。
二义性:直接访问顶层基类成员,编译器无法确定访问哪份副本,导致编译错误。
当一个派生类通过多条路径间接继承自同一个基类时,会导致基类成员在派生类中出现多份副本,引发二义性和数据冗余。
间接基类通过关键字 virtual 虚继承顶层基类,让间接基类在派生类中只保留一份实例,解决了这一问题。
虚继承原理:
虚继承的类添加虚基类表指针,指向虚基类表。
虚基类表存储当前类到虚基类的偏移量,中间基类访问时都指向同一份虚基类成员。
只需要额外的指针开销,访问虚基类成员需要计算偏移量。
最后更新于