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

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帮助管理和控制多态

相关文章:

  • 【Linux】进程地址空间
  • 免费轻量化办公pdf修改软件 一键格式转换基础修改到高级加密
  • keil+vscode+腾讯ai助手
  • 【笔记】【B站课程 pytorch】梯度下降模型
  • 深入理解 mapper-locations
  • LintCode407-加一,LintCode第479题-数组第二大数
  • MySQL - 事务
  • 5.2创新架构
  • 浔川AI 第二次内测报告
  • 浅析MySQL 的 **触发器(Trigger)** 和 **存储过程(Stored Procedure)原理及优化建议
  • c++学习合集(2025-4-29)
  • 基于Anaconda的Pycharm环境配置
  • 使用图像生成式AI和主题社区网站助力运动和时尚品牌的新产品设计和市场推广的点子和实现
  • 20250506让NanoPi NEO core开发板使用Ubuntu core16.04系统的TF卡启动
  • 中达瑞和便携式高光谱相机:珠宝鉴定领域的“光谱之眼”
  • 车载通信网络安全:挑战与解决方案
  • 【表设计】外键的取舍-分布式中逐渐消失的外键
  • 【十五】Mybatis动态SQL实现原理
  • 在Unity AR应用中实现摄像头切换功能
  • 2025年服务器技术全景解析:量子计算、液冷革命与未来生态构建
  • 上海市委政法委召开会议传达学习总书记重要讲话精神
  • 商务部:自5月7日起对原产于印度的进口氯氰菊酯征收反倾销税
  • “五一”假期文旅热度创近3年新高,入境游订单飙升130%
  • 李学明谈笔墨返乡:既耕春圃,念兹乡土
  • 特朗普关税风暴中的“稳”与“变”:新加坡国会选举观察
  • 三亚再回应游客骑摩托艇出海遇暴雨:俱乐部未配备足额向导人员,停业整改