【C++游记】物种多样——谓之多态

  

枫の个人主页

你不能改变过去,但你可以改变未来

算法/C++/数据结构/C

Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记

 话不多说,让我们一起进入今天的学习吧~~~  

一、多态的概念

多态(polymorphism)字面意思是“多种形态”,在C++中分为两类:编译时多态(静态多态)运行时多态(动态多态)

1. 编译时多态(静态多态)

核心是“编译期确定调用哪个函数”,主要通过函数重载函数模板实现。

原理:通过不同的参数类型/个数,编译器在编译阶段就确定要调用的函数版本,无需运行时判断。

// 函数重载示例(编译时多态)
#include <iostream>
using namespace std;// 加法函数:int类型
int add(int a, int b) {return a + b;
}// 加法函数:double类型(重载)
double add(double a, double b) {return a + b;
}int main() {cout << add(1, 2) << endl;    // 编译时确定调用int版本cout << add(1.5, 2.5) << endl;// 编译时确定调用double版本return 0;
}

2. 运行时多态(动态多态)

核心是“运行期确定调用哪个函数”,即“同一个行为,传入不同对象,产生不同结果”。

生活案例:
- 买票行为:普通人全价、学生打折、军人优先
- 动物叫行为:猫“喵”、狗“汪汪”

// 动物叫示例(运行时多态)
#include <iostream>
using namespace std;class Animal {
public:// 虚函数:关键标志virtual void talk() const {cout << "动物叫" << endl;}
};class Cat : public Animal {
public:// 重写虚函数virtual void talk() const override {cout << "(>^ω^<)喵" << endl;}
};class Dog : public Animal {
public:virtual void talk() const override {cout << "汪汪" << endl;}
};// 统一接口:接收基类引用
void letsHear(const Animal& animal) {animal.talk(); // 运行时确定调用哪个版本
}int main() {Cat cat;Dog dog;letsHear(cat); // 输出:(>^ω^<)喵letsHear(dog); // 输出:汪汪return 0;
}

注意:

本文重点讲解运行时多态,因为它是C++面向对象的核心,也是面试高频考点;编译时多态相对简单,日常开发中使用频率也较低。

二、多态的定义及实现

2.1 多态的构成条件

要实现运行时多态,必须同时满足以下两个核心条件:

  • 条件1:必须通过基类的指针或引用调用虚函数
  • 条件2:被调用的函数必须是虚函数,且派生类必须对基类的虚函数完成重写(覆盖)

为什么必须用基类指针/引用?

因为只有基类的指针或引用才能“兼容”指向基类对象和派生类对象,普通基类对象无法做到这一点(会发生切片,丢失派生类特性)。

2.1.1 虚函数

虚函数是多态的“开关”,定义方式:在类成员函数前加virtual关键字。

// 虚函数定义示例
class Person {
public:// 虚函数:加virtual关键字virtual void BuyTicket() {cout << "买票-全价" << endl;}// 注意:非成员函数不能加virtual(编译报错)// virtual void func() {} // 错误:全局函数不能是虚函数
};class Student : public Person {
public:// 派生类虚函数:建议显式加virtual(规范)virtual void BuyTicket() {cout << "买票-打折" << endl;}
};

注意:

1.virtual只能修饰类成员函数,不能修饰全局函数、静态成员函数、构造函数;

2. 派生类的虚函数可以省略virtual(因为继承后基类虚函数的“虚属性”会保留),但不建议这样写,会降低代码可读性。

2.1.2 虚函数的重写(覆盖)

重写(覆盖)是指:派生类中有一个与基类完全相同的虚函数(返回值类型、函数名、参数列表必须完全一致),此时派生类的虚函数会“覆盖”基类的虚函数。

// 虚函数重写示例
#include <iostream>
using namespace std;class Person {
public:// 基类虚函数virtual void BuyTicket() {cout << "Person: 买票-全价" << endl;}
};class Student : public Person {
public:// 派生类重写虚函数(返回值、函数名、参数列表完全一致)virtual void BuyTicket() override { // override关键字:检测重写是否正确cout << "Student: 买票-半价" << endl;}
};class Soldier : public Person {
public:// 派生类重写虚函数virtual void BuyTicket() override {cout << "Soldier: 买票-优先" << endl;}
};// 统一接口:基类指针
void Func(Person* ptr) {ptr->BuyTicket(); // 运行时确定调用哪个版本
}int main() {Person ps;Student st;Soldier sr;Func(&ps);  // 输出:Person: 买票-全价Func(&st);  // 输出:Student: 买票-半价Func(&sr);  // 输出:Soldier: 买票-优先return 0;
}

2.1.3 析构函数的重写(面试重点)

析构函数的重写是一个特殊场景:基类析构函数为虚函数时,派生类析构函数无论是否加virtual,都与基类析构函数构成重写

原因:编译器会将所有析构函数的名称统一处理为destructor,因此即使派生类析构函数名与基类不同,也能构成重写。

// 析构函数重写的重要性(避免内存泄漏)
#include <iostream>
using namespace std;class A {
public:// 基类析构函数:加virtualvirtual ~A() {cout << "~A()" << endl;}
};class B : public A {
public:B() {_p = new int[10]; // 动态申请内存}// 派生类析构函数:无需显式加virtual(但建议加)~B() override {delete[] _p; // 释放内存cout << "~B(): 释放了int数组" << endl;}private:int* _p;
};int main() {A* p1 = new A;A* p2 = new B;delete p1; // 输出:~A()(正确)delete p2; // 输出:~B(): 释放了int数组 → ~A()(正确,无内存泄漏)return 0;
}

面试必问:为什么基类析构函数建议设计为虚函数?

如果基类析构函数不是虚函数,当用基类指针指向派生类对象并删除时,只会调用基类析构函数,不会调用派生类析构函数,导致派生类中动态申请的内存无法释放,造成内存泄漏

2.2 易混淆概念对比

C++中重载、重写(覆盖)、重定义(隐藏)是三个容易混淆的概念,这里用表格清晰对比:

概念定义作用范围函数名参数列表返回值virtual关键字
重载同一作用域内的同名函数同一类相同不同可以不同无关
重写(覆盖)派生类重写基类的虚函数基类与派生类相同相同相同(协变除外)基类必须有,派生类可省略
重定义(隐藏)派生类与基类同名函数(非重写)基类与派生类相同可以相同/不同可以不同基类无virtual或参数不同
// 重载、重写、重定义对比示例
#include <iostream>
using namespace std;class Base {
public:// 重载:同一类中,函数名相同,参数不同void func() {cout << "Base::func()" << endl;}void func(int x) {cout << "Base::func(int x)" << endl;}// 虚函数:可被重写virtual void virtualFunc() {cout << "Base::virtualFunc()" << endl;}// 非虚函数:会被派生类重定义(隐藏)void nonVirtualFunc() {cout << "Base::nonVirtualFunc()" << endl;}
};class Derived : public Base {
public:// 重写(覆盖):重写基类虚函数virtual void virtualFunc() override {cout << "Derived::virtualFunc()" << endl;}// 重定义(隐藏):与基类nonVirtualFunc同名,参数相同但基类无virtualvoid nonVirtualFunc() {cout << "Derived::nonVirtualFunc()" << endl;}// 重定义(隐藏):与基类func同名但参数不同void func(double x) {cout << "Derived::func(double x)" << endl;}
};int main() {Derived d;Base* b = &d;b->func();           // 调用Base::func()b->func(10);         // 调用Base::func(int x)b->virtualFunc();    // 调用Derived::virtualFunc()(重写,多态)b->nonVirtualFunc(); // 调用Base::nonVirtualFunc()(非虚函数,不构成多态)d.func(3.14);        // 调用Derived::func(double x)d.Base::func();      // 显式调用基类被隐藏的函数return 0;
}

三、纯虚函数与抽象类

3.1 纯虚函数

纯虚函数是一种特殊的虚函数:在声明时初始化为0,且没有函数体。它的作用是强制派生类必须重写该函数

// 纯虚函数定义
class Shape {
public:// 纯虚函数:=0表示没有函数体virtual double area() const = 0; // 普通虚函数:可以有函数体virtual void printInfo() const {cout << "这是一个图形" << endl;}
};

3.2 抽象类

含有纯虚函数的类称为抽象类(也叫接口类)。抽象类有以下特性:

  • 抽象类不能实例化对象(无法创建具体实例)
  • 抽象类的派生类必须重写所有纯虚函数,否则该派生类仍为抽象类
  • 抽象类可以定义普通成员函数和成员变量
  • 可以声明抽象或引用(这是多态的基础)
// 抽象类示例
#include <iostream>
using namespace std;// 抽象类:含有纯虚函数
class Shape {
public:// 纯虚函数:计算面积virtual double area() const = 0;// 纯虚函数:计算周长virtual double perimeter() const = 0;// 普通成员函数void print() const {cout << "面积: " << area() << ", 周长: " << perimeter() << endl;}
};// 派生类:圆
class Circle : public Shape {
public:Circle(double r) : _radius(r) {}// 必须重写所有纯虚函数double area() const override {return 3.14 * _radius * _radius;}double perimeter() const override {return 2 * 3.14 * _radius;}private:double _radius; // 半径
};// 派生类:矩形
class Rectangle : public Shape {
public:Rectangle(double w, double h) : _width(w), _height(h) {}// 必须重写所有纯虚函数double area() const override {return _width * _height;}double perimeter() const override {return 2 * (_width + _height);}private:double _width;  // 宽double _height; // 高
};// 多态应用:统一接口操作不同图形
void showShapeInfo(const Shape& shape) {shape.print();
}int main() {// 错误:抽象类不能实例化对象// Shape shape; // 正确:可以声明抽象类的指针/引用Circle circle(5.0);Rectangle rect(3.0, 4.0);showShapeInfo(circle); // 输出:面积: 78.5, 周长: 31.4showShapeInfo(rect);   // 输出:面积: 12, 周长: 14return 0;
}

抽象类的应用场景:

当我们需要定义一个基类,但不希望它被实例化,只作为派生类的接口规范时,就可以使用抽象类。例如:

  • 图形类(Shape):派生类可以是圆形、矩形、三角形等
  • 动物类(Animal):派生类可以是猫、狗、鸟等
  • 设备类(Device):派生类可以是打印机、扫描仪、投影仪等

四、多态的底层原理

C++多态的底层是通过虚函数表(Virtual Table,简称vtable)虚表指针(vpointer,简称vptr)实现的。理解这一机制,能帮你更深入地掌握多态的本质。

4.1 虚函数表(vtable)

当一个类中含有虚函数时,编译器会为该类创建一个虚函数表

  • 虚函数表是一个函数指针数组,存储该类所有虚函数的地址
  • 每个含有虚函数的类只有一个虚函数表(所有对象共享)
  • 派生类会继承基类的虚函数表,如果重写了基类的虚函数,会用派生类自己的函数地址覆盖虚表中对应的位置
  • 如果派生类有新的虚函数,会被添加到虚函数表的末尾

4.2 虚表指针(vptr)

每个含有虚函数的类的对象,都会有一个虚表指针(vptr)

  • 虚表指针是对象的第一个成员(存储在对象内存的最前面)
  • 虚表指针指向该类的虚函数表
  • 对象创建时,编译器会自动初始化vptr,使其指向相应的虚函数表

4.3 多态的实现过程

当通过基类指针/引用调用虚函数时,多态的实现过程如下:

  1. 通过基类指针/引用访问对象的虚表指针(vptr)
  2. 通过vptr找到该对象所属类的虚函数表(vtable)
  3. 在虚函数表中找到对应虚函数的地址
  4. 调用该地址指向的函数(即派生类重写后的函数)
// 多态底层原理示例
#include <iostream>
using namespace std;class Base {
public:virtual void func1() { cout << "Base::func1()" << endl; }virtual void func2() { cout << "Base::func2()" << endl; }void func3() { cout << "Base::func3()" << endl; } // 非虚函数
private:int _b = 1;
};class Derived : public Base {
public:virtual void func1() override { cout << "Derived::func1()" << endl; }virtual void func3() { cout << "Derived::func3()" << endl; } // 新的虚函数
private:int _d = 2;
};int main() {Base b;Derived d;// 注意:以下输出结果可能因编译器不同而略有差异cout << "Base对象大小: " << sizeof(b) << endl;  // 输出:8(4字节_b + 4字节vptr)cout << "Derived对象大小: " << sizeof(d) << endl;// 输出:12(4字节_b + 4字节_d + 4字节vptr)return 0;
}

上述代码的虚函数表结构如下:

Base类的虚函数表
  • vtable[0] → &Base::func1
  • vtable[1] → &Base::func2
Derived类的虚函数表
  • vtable[0] → &Derived::func1(覆盖基类的func1)
  • vtable[1] → &Base::func2(继承基类的func2)
  • vtable[2] → &Derived::func3(新增的虚函数)

注意:

1. 虚函数表是编译器在编译期生成的,存储在只读数据段(.rodata);

2. 虚表指针是在对象构造时初始化的,指向所属类的虚函数表;

3. 多态会带来轻微的性能开销(多一次指针间接访问),但通常可以忽略不计。

五、常见问题与面试题

Q1:静态成员函数可以是虚函数吗?

A:不可以。因为静态成员函数属于类,不属于某个具体对象,没有this指针,而虚函数的调用需要通过对象的vptr找到vtable,因此静态成员函数不能是虚函数。

Q2:构造函数可以是虚函数吗?

A:不可以。因为对象的vptr是在构造函数执行时初始化的,在构造函数还未执行时,vptr尚未指向正确的虚函数表,因此构造函数不能是虚函数。

Q3:析构函数为什么要设为虚函数?

A:如前文所述,当用基类指针指向派生类对象并删除时,如果基类析构函数是虚函数,会先调用派生类析构函数,再调用基类析构函数,确保资源正确释放;否则只会调用基类析构函数,导致派生类资源泄漏。

Q4:多态有什么优缺点?

A:优点:
1. 提高代码的复用性和可维护性;
2. 提高代码的扩展性,新增派生类不影响原有代码;
3. 接口统一,使用者无需关心具体实现。
缺点:
1. 增加了系统复杂度;
2. 带来轻微的性能开销(虚函数调用需要查表);
3. 可能隐藏错误,调试难度增加。

Q5:什么情况下会发生隐藏(重定义)?

A:派生类中的函数与基类中的函数同名,且不构成重写时,会发生隐藏:
1. 基类函数不是虚函数,派生类函数与基类函数同名(无论参数是否相同);
2. 基类函数是虚函数,但派生类函数与基类函数参数不同。

Q6:如何判断一段代码是否构成多态?

A:同时满足以下条件:
1. 存在继承关系;
2. 基类中存在虚函数,派生类重写了该虚函数;
3. 通过基类的指针或引用调用该虚函数。

六、总结

  • 多态分为编译时多态(函数重载、模板)和运行时多态(虚函数);
  • 运行时多态的实现条件:基类指针/引用 + 虚函数重写
  • 虚函数重写要求:函数名、参数列表、返回值完全相同(协变除外);
  • override关键字用于检查重写是否正确,final关键字用于禁止重写或继承;
  • 含有纯虚函数的类是抽象类,不能实例化,派生类必须重写所有纯虚函数;
  • 多态的底层是通过虚函数表(vtable)虚表指针(vptr)实现的;
  • 基类析构函数建议设为虚函数,避免派生类对象销毁时的内存泄漏;

七、结语

今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/94651.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94651.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Visual Scope (Serial_Digital_Scope V2) “串口 + 虚拟示波器” 工具使用记录

VisualScope 就是一个 “串口 + 虚拟示波器” 的工具,适合在没有昂贵示波器/逻辑分析仪时做嵌入式调试。它的核心步骤就是 MCU 定时发数据 → PC 串口接收 → 软件画波形。 首先准备串口通信工具后,插入电脑,安装完USB转串口驱动后,在“我的电脑”-“设备及管理器”-“端口…

c++ 观察者模式 订阅发布架构

#include <iostream> #include <vector> #include <algorithm> #include <memory> #include <mutex>// 观察者接口 class IObserver { public:virtual ~IObserver() default;virtual void update(const std::string& message) 0; };// 主题…

oracle 表空间扩容(增加新的数据文件)

SELECT tablespace_name,file_name,ROUND(bytes / 1024 / 1024, 2) AS size_mb,ROUND(maxbytes / 1024 / 1024, 2) AS max_size_mb,status,autoextensible FROM dba_data_files ORDER BY tablespace_name;--给表空间增加一个新数据库文件ALTER TABLESPACE EAS_D_EAS_STANDARDAD…

DAY 58 经典时序预测模型2

知识点回顾&#xff1a; 时序建模的流程时序任务经典单变量数据集ARIMA&#xff08;p&#xff0c;d&#xff0c;q&#xff09;模型实战SARIMA摘要图的理解处理不平稳的2种差分 n阶差分---处理趋势季节性差分---处理季节性 昨天我们掌握了AR, MA, 和 ARMA 模型&#xff0c;它们…

【人工智能】AI代理重塑游戏世界:动态NPC带来的革命性沉浸式体验

还在为高昂的AI开发成本发愁?这本书教你如何在个人电脑上引爆DeepSeek的澎湃算力! 在当今游戏行业迅猛发展的时代,AI代理技术正悄然引发一场革命,尤其是动态非玩家角色(NPC)的应用,将传统静态游戏体验提升至全新的沉浸式境界。本文深入探讨AI代理在游戏中的核心作用,从…

服务器关机故障排查:大白话版笔记

注意:本文解释文字仅供学习交流使用,不构成专业的技术指导或建议;只是理论实例解释不代表实际运维场景操作,注意鉴别! 运维日常最头疼的就是服务器 “突然躺平” —— 要么没操作就自己关机,要么想关还关不掉。 紧急检查清单 (Cheat Sheet) 服务器突然宕机,重启后第一…

如何通过docker进行本地部署?

如何通过docker进行本地部署&#xff1f; 在做项目的过程中&#xff0c;想要上线项目的话肯定是不能在我们电脑上进行开发的&#xff0c;要部署到服务器上面&#xff0c;今天就总结一下操作步骤。 1、创建springboot项目 随便创建一个springboot工程&#xff0c;确保control…

解锁AI“黑匣”:监督、无监督与强化学习探秘

在当今数字化浪潮汹涌澎湃的时代&#xff0c;AI 决策已然成为推动各领域变革与发展的核心驱动力&#xff0c;从智能语音助手到自动驾驶汽车&#xff0c;从医疗诊断辅助到金融风险预测&#xff0c;AI 决策的身影无处不在&#xff0c;深刻地改变着人们的生活与工作方式。​AI 决策…

F008 vue+flask 音乐推荐评论和可视化系统+带爬虫前后端分离系统

文章结尾部分有CSDN官方提供的学长 联系方式名片 文章结尾部分有CSDN官方提供的学长 联系方式名片 关注B站&#xff0c;有好处&#xff01; F008 &#x1f3b6;vueflask 音乐推荐和可视化系统带爬虫前后端分离系统 编号&#xff1a;F008 B站视频介绍&#xff1a; vueflask-云音…

海盗王64位dx9客户端修改篇之二

目前全网&#xff0c;估计也就只有这个是海盗王客户端3.0的原始版直接升级成64位dx9的了。客户端非常简洁&#xff0c;连64位lua都集成进去&#xff0c;除了新更换的64位SDL音乐播放库dll没办法集成外&#xff0c;没有任何多余的其他文件了。 之前有其他大佬将1.38的改成了dx9的…

点评项目(Redis中间件)第二部分Redis基础

Redis的java客户端spring整合了前三种我们只需要学习spring整合的就行了。不过还是有企业使用老一套的原生的jedis。jedis操作引入依赖<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.7.0</ve…

LeetCode-19day:贪心算法

贪心算法经典题目总结&#xff08;C实现&#xff09; 贪心算法是一种在每一步选择中都采取当前状态下最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最优的算法。本文总结了四道经典的贪心算法问题&#xff0c;帮助你更好地理解和掌握贪心算法…

Microsoft Edge WebView2 Runtime:为应用程序提供浏览器核心功能

在现代软件开发中&#xff0c;嵌入网页内容到应用程序界面是一个常见的需求。Microsoft Edge WebView2 Runtime&#xff08;WebView2运行库&#xff09;作为微软操作系统WebView2控件的运行环境&#xff0c;基于Chromium内核构建&#xff0c;为应用程序提供了浏览器核心功能&am…

PDF文件中的相邻页面合并成一页,例如将第1页和第2页合并,第3页和第4页合并

PDF页面合并工具 这个工具可以将PDF文件中的相邻页面合并成一页&#xff0c;例如将第1页和第2页合并&#xff0c;第3页和第4页合并&#xff0c;以此类推。 功能 自动检测PDF文件中的页面数量将相邻的页面合并成一页处理奇数页数的PDF文件&#xff08;最后一页单独保留&#xff…

git hub初使用问题记录

问题一、Connection closed by UNKNOWN port 65535设置config文件为Host github.com Hostname ssh.github.com Port 443 User git问题二、ERROR: Repository not found.fatal: Could not read from remote repository.Please make sure you have the correct access rightsand …

解读 AUTOSAR AP R24-11 Manifest 规范 —— 从部署到安全的全流程支撑

今天我们来拆解 AUTOSAR AP R24-11 版本的《Requirements on Manifest Specification》Manifest 规范要求—— 这份文档是 Adaptive 平台软件 “落地运行” 的核心指南,它解决了一个关键问题:如何让 AP 软件在车载 ECU 上安全、可靠地部署和通信? 自适应平台(AP)是啥? 是…

Linux系统 -- 多线程的控制(互斥与同步)

在多线程编程中&#xff0c;多个线程可能同时访问临界资源&#xff08;如共享变量、文件、硬件设备等&#xff09;&#xff0c;若缺乏控制会导致数据混乱。互斥和同步是解决该问题的核心机制&#xff0c;其中互斥锁保证临界资源的排他访问&#xff0c;信号量实现线程间的有序协…

一键搭建开发环境:制作bash shell脚本

完整脚本&#xff1a; 1.0 #!/bin/bash set -eecho " 开始安装 AI 开发环境&#xff08;无人交互版&#xff09; "# 检测是否以 sudo 运行 if [ "$EUID" -eq 0 ]; thenecho "⚠️ 警告&#xff1a;请不要使用 sudo 运行此脚本&#xff01;"echo …

mac m4执行nvm install 14.19.1报错,安装低版本node报错解决

原因 由于node14使用的变异工具链太旧&#xff0c;无法适配最新的macOS SDK头文件导致_studio.h报错 解决办法 方法1 更新nvm到最新版本 brew update nvmnvm install 14.19.1 --binary 方法2 启用Rosetta安装&#xff08;Intel仿真&#xff09; 1.arch -x86_64 zsh 2.nvm insta…

Codeforces Round 1043 (Div. 3) F. Rada and the Chamomile Valley

F.拉达和甘菊谷 每次测试的时间限制&#xff1a;3 秒 每次测试的内存限制512 兆字节 输入&#xff1a;标准输入 输出&#xff1a;标准输出 昨天&#xff0c;拉达发现了一个传送门&#xff0c;可以把她传送到洋甘菊谷&#xff0c;然后再传送回来。拉达的快乐无以言表&#xff0c…