当前位置: 首页 > web >正文

C++多态详解

1. 多态的概念

多态性允许我们通过基类的指针或引用访问派生类的成员函数,使得同一个函数调用可以有不同的行为。

多态 = “一个名字,多种行为”。

例如:

票价的售卖,同样是售卖门票,但是针对不同的人群有不同的票价。

语言的种类,同样是说话,不同国家的人有不同的语言。

多态分为两种类型:

  • 静态多态(编译时多态):通过函数重载和运算符重载实现
  • 动态多态(运行时多态):通过虚函数实现

1.1静态多态(编译时多态)

编译器在编译时直接根据参数类型挑选对应的函数,然后将函数调用“替换成”具体的机器指令。运行时没有任何判断、跳转、查表的过程,效率极高

常见实现方式:

  • 函数重载(Function Overload)

  • 运算符重载(Operator Overload)

  • 类模板

1.2动态多态(运行时多态)

动态多态发生在程序运行时,即程序执行过程中才根据对象的实际类型决定要调用哪个函数。

关键要素:

  1. 基类中的函数必须是 virtual(虚函数)

  2. 派生类中要重写这个虚函数

  3. 使用基类的指针或引用调用这个函数

底层原理:

当类中出现虚函数时,编译器会为这个类创建一张虚函数表(vtable),每个对象中还会有一个指针 vptr 指向对应的表。但是一般父类创建虚函数表后,子类无需额外创建,直接继承父类的虚函数表,若在子类中对虚函数进行重写后,会在子类虚函数表中覆盖原来父类该函数的地址。

运行时,程序会:

  1. 通过对象的 vptr 找到 vtable

  2. 在表中查找到对应的函数地址

  3. 跳转执行这个地址的函数(比如 Chinese::Speak)

所以 p->Speak() 虽然 p是 Person* 类型,但它内部的 vptr 指向的是 Chinese的函数表,所以调用的是Chinese::Speak()

2. 实现多态的条件

2.


2. 实现多态的条件

实现动态多态需要满足以下两个条件,缺一不可:

  1. 虚函数的重写:派生类重写基类的虚函数

  2. 基类指针或引用调用虚函数:使用基类的指针或引用指向派生类对象,并通过该指针或引用调用虚函数

#include <iostream>
using namespace std;class Person {
public:virtual void BuyTicket() {cout << "全价购票" << endl;}
};class Student : public Person {
public:virtual void BuyTicket() { // 重写基类的虚函数cout << "半价购票" << endl;}
};void Show(Person& p) { // 通过基类引用调用虚函数p.BuyTicket();// 这里发生了隐式向上类型转换:将派生类对象转换为基类引用// 原因是派生类继承了基类的所有公共和受保护成员,因此派生类对象可以被安全地视为基类对象的一种
}int main() {Person p;Student s;Show(p); // 输出: 全价购票Show(s); // 输出: 半价购票return 0;
}

在这段代码中:Person类中定义了一个虚函数BuyTicket()Student类继承自Person并重写了BuyTicket()函数,函数Show()接受一个Person类型的引用,并调用其BuyTicket()方法,当我们传递Student对象时,会调用Student类的BuyTicket()方法,这就是多态的表现。

3.虚函数的重写规则

虚函数重写必须满足以下条件:

  1. 函数名相同
  2. 参数列表相同(参数类型和数量)
  3. 返回值类型相同(有特例:协变)
  4. 访问修饰符可以不同,但不能降低访问权限

3.1协变

C++允许在派生类中重写的虚函数返回类型与基类虚函数返回类型不同,这种特殊情况称为"协变返回类型"。协变要求:

  1. 基类虚函数返回基类对象的指针或引用

  2. 派生类虚函数返回派生类对象的指针或引用(派生类是基类的子类)

class Person {
public:virtual Person* BuyTicket() {cout << "全价购票" << endl;return this;}
};class Student : public Person {
public:virtual Student* BuyTicket() { // 协变返回类型cout << "半价购票" << endl;return this;}
};

注意:在重写基类虚函数时,派生类的虚函数前面的virtual关键字可以不加,编译器会自动处理这种情况。但为了代码可读性,建议显式添加virtual关键字。

4. 虚函数表和多态的实现原理

每个包含虚函数的类都有一个虚函数表,这个表是一个指针数组,存储了该类所有虚函数的地址:

  1. 虚函数表:每个类拥有一个虚函数表,存储类中所有虚函数的地址

  2. 虚函数表指针(vptr):每个对象都有一个虚表指针,指向该对象所属类的虚函数表

  3. 动态绑定:当通过基类指针或引用调用虚函数时,会根据对象的实际类型查找对应的虚函数表,找到并调用正确的函数

重要细节:子类本身没有独立的虚表指针,而是包含在从父类继承下来的对象中。当子类重写父类的虚函数时,子类的虚表会覆盖原来函数地址,而父类的虚表保持不变,仍存储父类的函数地址。

我们可以通过以下代码查看虚函数表的内容:

typedef void(*FuncPtr)(); // 定义函数指针类型// 打印虚函数表的函数
void PrintVFTable(FuncPtr* pVTable) {for (size_t i = 0; pVTable[i] != 0; i++) {printf("pVTable[%d]:%p->", i, pVTable[i]);FuncPtr f = pVTable[i];f(); // 调用函数}cout << endl;
}class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int _a;
};class Derive : public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; } // 重写virtual void func3() { cout << "Derive::func3" << endl; } // 新增
private:int _b;
};void test() {Base b;Derive d;// 获取并打印虚函数表PrintVFTable((FuncPtr*)(*(size_t*)&b)); //取出对象的地址后强转为整数指针进行解引用后得到对PrintVFTable((FuncPtr*)(*(size_t*)&d)); //象内部虚函数表的地址后强转为(FuncPtr*)}

5. 虚析构函数

当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,则只会调用基类的析构函数,而不会调用派生类的析构函数,这可致内能导存泄漏。因此,当类设计用作基类时,通常应该将其析构函数声明为虚函数:

class Person {
public:virtual ~Person() {cout << "~Person()" << endl;}
};class Student : public Person {
public:virtual ~Student() { // 析构函数的函数名会被编译器内部处理成destructor//无需显式调用基类的析构函数,派生类析构函数结束后会自动调用基类析构函数cout << "~Student()" << endl;}
};int main() {Person* p = new Student();delete p; // 会先调用Student的析构函数,再调用Person的析构函数return 0;
}

6.纯虚函数和抽象类

6.1 纯虚函数

纯虚函数是一种特殊的虚函数,它在基类中没有实现,要求派生类必须提供实现:

virtual 返回类型 函数名(参数列表) = 0;

6.2 抽象类

包含至少一个纯虚函数的类称为抽象类。抽象类有以下特点:

  1. 不能实例化对象,但可以声明指针和引用

  2. 必须在派生类中实现所有纯虚函数,否则派生类也是抽象类

class Car {
public:virtual void Run() = 0; // 纯虚函数virtual void Stop() = 0; // 纯虚函数
};class Benz : public Car {
public:virtual void Run() {cout << "Benz Run" << endl;}virtual void Stop() {cout << "Benz Stop" << endl;}
};void test() {// Car c; // 错误:不能创建抽象类的对象Car* p = new Benz(); // 正确:可以创建抽象类的指针p->Run(); // 输出: Benz Runp->Stop(); // 输出: Benz Stopdelete p;
}

7. 虚函数的默认参数

虚函数可以有默认参数,但默认参数是静态绑定的,即根据指针或引用的类型决定使用哪个默认参数,而不是根据对象的实际类型:

class Base {
public:virtual void show(int x = 10) {cout << "Base::show " << x << endl;}
};class Derived : public Base {
public:void show(int x = 20) override {cout << "Derived::show " << x << endl;}
};int main() {Derived d;Base* p = &d;p->show();     // 输出:Derived::show 10  <- 默认值是Base的d.show();      // 输出:Derived::show 20 <- 默认值是Derived的
}

p的类型是Base*,当编译器在编译这一行时会查Base::show(int x = 10),看到默认值是 10,就把它“写死”成了 p->show(10);

总的来说:默认参数“看你用什么类型的指针”,而不是“你指向的对象是谁”

8. final 和 override 关键字

8.1 final 关键字

final关键字可以防止类被继承或虚函数被重写:

class Base {
public:virtual void func() final { // 禁止重写此函数cout << "Base::func" << endl;}
};class Derive final : public Base { // 禁止继承此类// virtual void func() { } // 错误:不能重写被final修饰的函数
};// class Further : public Derive { }; // 错误:不能继承被final修饰的类

8.2 override 关键字

override关键字用于明确表示函数是对基类虚函数的重写,如果不满足重写条件,编译器会报错:

class Base {
public:virtual void func() {cout << "Base::func" << endl;}
};class Derive : public Base {
public:virtual void func() override { // 明确表示重写基类的func函数cout << "Derive::func" << endl;}// virtual void funk() override { } // 错误:基类没有名为funk的虚函数
};

9. 重载、重写和重定义对比

特性重载(Overload)重写(Override)重定义 / 隐藏(Hide)
是否发生继承关系否,通常在同一个类中是,派生类对基类虚函数的重新实现是,发生在派生类中
是否函数名相同✅ 是✅ 是✅ 是
参数列表不同(个数或类型不同)相同不同也可触发隐藏
返回值类型可不同通常相同(支持协变返回类型)可不同
是否必须是虚函数✅ 是(基类中必须是 virtual否(可是虚函数,也可以不是)
与虚函数表关系无关✅ 修改虚函数表(vtable),支持运行时多态❌ 不修改虚函数表,隐藏基类版本
编译时 or 运行时编译时绑定运行时绑定(通过虚函数表)编译时绑定(隐藏而非替换)
使用方式同类中多个同名函数(重载)派生类中重写基类的虚函数(覆盖)派生类中定义新函数,与基类同名但参数不同或非虚函数
class Base {
public:void func() {cout << "Base::func()" << endl;}virtual void vfunc() {cout << "Base::vfunc()" << endl;}
};class Derive : public Base {
public:void func() { // 重定义(隐藏)cout << "Derive::func()" << endl;}virtual void vfunc() { // 重写cout << "Derive::vfunc()" << endl;}
};void test() {Base* pb = new Derive();pb->func(); // 输出: Base::func() - 静态绑定pb->vfunc(); // 输出: Derive::vfunc() - 动态绑定delete pb;
}

10.总结

C++的多态是面向对象编程的重要特性,通过虚函数实现:

  1. 多态的实现:虚函数 + 继承 + 基类指针/引用
  2. 虚函数表:多态的实现机制,每个含有虚函数的类都有一个虚函数表,子类重写虚函数会覆盖虚表中的函数地址
  3. 纯虚函数与抽象类:定义接口,强制派生类实现特定功能
  4. 虚析构函数:防止内存泄漏的重要手段
  5. 关键字virtual在多态中用于定义虚函数,在菱形继承中用于虚继承;overridefinal帮助管理和控制多态
http://www.xdnf.cn/news/4104.html

相关文章:

  • ORCAD打印pdf
  • Docker手动重构Nginx镜像,融入Lua、Redis功能
  • 【C++】WSL常用语法
  • 先滤波再降采样 还是 先降采样再滤波
  • IL2CPP 技术深度解析
  • std::move()详解
  • n8n 使用 Merge 节点进行数据聚合
  • 系统思考:困惑源于内心假设
  • 【AI入门】Cherry入门1:Cherry Studio的安装及配置
  • suna工具调用可视化界面实现原理分析(一)
  • 使用Mathematica绘制Sierpinski地毯
  • 观察者模式(Observer Pattern)
  • 解锁DeepSeek模型微调:从小白到高手的进阶之路
  • 【AND-OR-~OR锁存器设计】2022-8-31
  • AfuseKt2.4.2 | 支持阿里云盘、Alist等平台视频播放,具备自动海报墙刮削功能的强大播放器
  • GEMM inTriton (Split-K and Stream-K)
  • 经典的 Masked + Self-supervised learning 的模型方法
  • 学习路线(视觉)
  • Deep-Live-Cam-实时换脸开源部署和使用
  • sqli-labs靶场11-17关(POST型)
  • 小白学习java第16天(下):javaweb
  • 【C/C++】inline关键词
  • 第六章:6.1 ESP32教学:多任务处理与FreeRTOS实战
  • 谷歌SMR测试环境搭建
  • Spring 框架中 @Configuration 注解详解
  • Springboot循环依赖
  • FOC算法开环控制基础
  • Java开发者面试实录:微服务架构与Spring Cloud的应用
  • 学习黑客Nmap 原理
  • 什么是外联模板(extern template)?