多态

基本概念

多态(Polymorphism)是面向对象编程的核心特性之一,它允许同一个接口表现出不同的行为。在C++中,多态分为两种主要类型:编译时多态运行时多态

多态的字面意思是"多种形态",在编程中指的是同一个操作作用于不同的对象时,可以有不同的解释和执行结果。多态提供了一种统一的接口来处理不同类型的对象。

编译时多态(Static Polymorphism)也称为静态多态,在编译期间就确定了具体调用哪个函数。主要包括:

  1. 函数重载(Function Overloading)

  2. 运算符重载(Operator Overloading)

  3. 模板(Templates)

  4. 函数模板特化

运行时多态(Dynamic Polymorphism)也称为动态多态,在运行时才确定具体调用哪个函数。主要通过虚函数实现。

  1. 基本虚函数

  2. 纯虚函数和抽象类

特性编译时多态运行时多态
决定时机编译期运行期
实现方式函数重载、运算符重载、模板虚函数、继承
性能高(无运行时开销)较低(虚函数调用开销)
灵活性低(编译时固定)高(运行时可变)
内存开销无额外开销需要vtable和vptr
类型安全编译时检查运行时检查
代码复用通过模板实现通过继承实现

编译时多态运行时多态各有优势:

虚函数的实现原理

实现机制

虚函数表(Virtual Function Table, vtable)

虚函数表是一个函数指针数组,存储着类中所有虚函数的地址。每个包含虚函数的类都有一个对应的虚函数表。

虚指针(Virtual Pointer, vptr)

虚指针是每个对象中的一个隐藏成员,指向该对象所属类的虚函数表。

基本结构

内存布局

虚函数表结构

当通过基类指针调用虚函数时:

调用过程:

  1. 获取vptr:从对象中读取虚指针

  2. 查找vtable:通过vptr找到对应的虚函数表

  3. 索引函数:根据函数在vtable中的索引找到函数地址

  4. 调用函数:通过函数指针调用实际的函数

多重继承

在多重继承中,情况更加复杂:

虚继承

虚继承会引入更复杂的vtable结构:

Important

  1. 每个包含虚函数的类都有一个vtable

  2. 每个对象都有一个vptr指向其类的vtable

  3. 虚函数调用通过vptr和vtable实现动态绑定

  4. 派生类的vtable继承并可能覆盖基类的虚函数

  5. 虚函数调用有一定的性能开销

  6. 现代编译器会尝试优化虚函数调用

动态多态被激活的条件

基本条件

条件1:基类中必须有虚函数

条件2:派生类必须重写(override)基类的虚函数

条件3:必须通过基类指针或引用调用虚函数

虚函数是动态多态的基础,它告诉编译器:

动态多态不被激活的情况

情况1:没有虚函数

情况2:直接通过对象调用

情况3:在构造函数或析构函数中调用虚函数

示例

输出结果

Important

  1. 基类有虚函数:使用virtual关键字

  2. 派生类重写虚函数:函数签名必须完全匹配

  3. 通过基类指针或引用调用:不能直接通过对象调用

  4. public继承:确保类型转换的合法性

虚函数的限制

1.构造函数不能设为虚函数

构造函数的作用是创建对象,完成数据的初始化,而虚函数机制被激活的条件之一就是要先创建对象,有了对象才能表现出动态多态。如果将构造函数设为虚函数,那此时构造未执行完,对象还没创建出来,存在矛盾。

2.静态成员函数不能设为虚函数

虚函数的实际调用:this -> vfptr -> vtable -> virtual function,但是静态成员函数没有this指针,所以无法访问到vfptr。

vfptr是属于一个特定对象的部分,虚函数机制起作用必然需要通过vfptr去间接调用虚函数。静态成员函数找不到这样特定的对象。

3.Inline函数不能设为虚函数

因为inline函数在编译期间完成替换,而在编译期间无法展现动态多态机制,所以起作用的时机是冲突的。如果同时存在,inline失效。

4.普通函数不能设为虚函数

虚函数要解决的是对象多态的问题,与普通函数无关。

抽象类

抽象类是面向对象编程中的一个重要概念,它定义了一个不能被实例化的类,通常用作其他类的基类来定义通用接口。

定义

在C++中,抽象类是包含至少一个纯虚函数的类。纯虚函数是在声明时被赋值为0的虚函数。

基本语法

特点1:不能被实例化

特点2:可以有指针和引用

特点3:派生类必须实现所有纯虚函数

示例

图形类

动物类

总结

Important

  1. 包含至少一个纯虚函数

  2. 不能被实例化

  3. 可以包含普通成员函数和数据成员

  4. 派生类必须实现所有纯虚函数才能被实例化

  5. 支持多态机制

与其他概念的区别

特性抽象类普通基类接口(纯抽象类)
纯虚函数至少一个可有可无全部都是
实例化不可以可以不可以
普通成员可以有可以有通常没有
多重继承支持支持常用于多重继承

带虚函数的多继承

基本情况

在多继承中,当基类包含虚函数时,派生类会继承这些虚函数,并可以重写它们。每个包含虚函数的基类都会有自己的虚函数表(vtable)。

派生类对象包含多个vtable指针(vptr),每个基类子对象都有自己的vptr:

vtable内容

Thunk机制

由于多继承中基类子对象的地址偏移,编译器使用thunk来调整this指针:

虚函数调用过程

调用过程:

  1. p1->func1():直接通过Base1的vtable调用Derived::func1

  2. p2->func2():通过Base2的vtable和thunk调用Derived::func2

  3. 同名虚函数common()在两个vtable中都指向Derived::common

菱形继承

在虚继承中:

示例

虚拟继承

虚拟继承(Virtual Inheritance)是C++中用来解决多重继承中菱形继承问题(Diamond Problem)的重要机制。它确保在复杂的继承层次结构中,共同的基类只有一个实例。

菱形继承问题

在上述代码中,Derived类继承了两个Base实例:

这导致:

  1. 内存浪费Base的数据被重复存储

  2. 二义性:访问Base成员时不知道访问哪一个

  3. 逻辑错误:违反了"一个对象一个基类实例"的原则

虚拟继承的解决方案

内存布局

普通多重继承的内存布局

虚拟继承的内存布局

构造和析构顺序

构造顺序

重要规则:

  1. 虚基类最先构造Base首先被构造

  2. 最派生类负责Derived直接调用Base的构造函数

  3. 中间类的Base调用被忽略LeftRight中的Base(v)调用被忽略

析构顺序

总结

虚拟继承是C++中解决菱形继承问题的重要机制:

优点:

缺点: