C++第14课:C++类的多态性(c++的多态性是指)
liuian 2025-07-28 18:13 51 浏览
1.概念与分类
C++ 类的多态性实现了 “一个接口,多种方法”的开发模式。同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。就像一个遥控器(接口),可以控制不同品牌的电视(对象),虽然操作方式一样(按下电源键),但每台电视的响应(开机画面、启动速度等)可能不同。在 C++ 中,多态性主要分为编译时多态和运行时多态。
1.1.编译时多态
编译时多态也叫静态多态,是在编译阶段就确定函数调用的版本。这种多态性主要通过函数重载和模板来实现。函数重载是指在同一个类中,多个函数可以具有相同的函数名,但参数列表(参数类型、个数或顺序)不同。如定义一个print函数,既可以打印整数,也可以打印字符串:
// 打印整数void print(int num) {std::cout << "打印整数: " << num << std::endl;}// 打印字符串void print(const char* str) {std::cout << "打印字符串: " << str << std::endl;}int main() {print(10); // 调用打印整数的版本print("Hello, C++"); // 调用打印字符串的版本return 0;}
在这个例子中,编译器会根据传入参数的类型来决定调用哪个print函数,这就是编译时多态的体现。模板则允许我们编写泛型代码,在编译时根据具体的类型实例化相应的函数或类,进一步增强了代码的复用性和灵活性。
编译时多态的优点是效率高,因为函数调用的版本在编译阶段就已经确定,不需要在运行时进行额外的开销。但是,它的灵活性相对较低,一旦编译完成,函数调用的版本就固定下来了。
1.2.运行时多态
运行时多态也叫动态多态,是在程序运行时根据对象的实际类型来决定函数调用的版本。它主要通过虚函数和继承来实现。当基类中定义了虚函数,派生类可以重写(override)这个虚函数。在运行时,通过基类指针或引用调用虚函数时,会根据指针或引用实际指向的对象类型,来决定调用哪个类的虚函数版本 。例如:
public:// 虚函数virtual void speak() const {std::cout << "动物发出声音" << std::endl;}};class Dog : public Animal {public:// 重写虚函数void speak() const override {std::cout << "狗汪汪叫" << std::endl;}};class Cat : public Animal {public:// 重写虚函数void speak() const override {std::cout << "猫喵喵叫" << std::endl;}};int main() {Animal* animal1 = new Dog();Animal* animal2 = new Cat();animal1->speak(); // 输出 "狗汪汪叫"animal2->speak(); // 输出 "猫喵喵叫"delete animal1;delete animal2;return 0;}
在这个例子中,Animal类中的speak函数被声明为虚函数,Dog类和Cat类重写了speak函数。通过Animal类型的指针调用speak函数时,实际调用的是指针所指向对象的具体类型(Dog或Cat)的speak函数版本,这就是运行时多态的体现。
2.函数重载与模版
2.1.函数重载
函数重载是指在同一个作用域内,定义多个同名函数,但这些函数的参数列表(参数个数、类型或顺序)不同。编译器会根据调用函数时传入的实际参数,在编译阶段选择合适的函数版本进行调用。例如:
void print(int num) {std::cout << "打印整数: " << num << std::endl;}void print(double num) {std::cout << "打印双精度浮点数: " << num << std::endl;}
在上述代码中,我们定义了两个名为print的函数,一个接受int类型参数,另一个接受double类型参数。当我们调用print(5)时,编译器会根据参数类型选择第一个print函数;当调用print(3.14)时,编译器会选择第二个print函数。
2.2.模版
模板包括函数模板和类模板,它允许开发者编写通用的代码,根据不同的类型参数生成不同的函数或类。例如,下面是一个简单的函数模板:
template<typename T>T add(T a, T b) {return a + b;}
这个函数模板可以用于不同类型的加法运算。当我们调用add(3, 5)时,编译器会根据传入的参数类型int,生成一个专门处理int类型的add函数;当调用add(3.14, 2.71)时,编译器会生成处理double类型的add函数。
3.虚函数
虚函数是 C++ 运行时多态的核心机制。简单来说,虚函数是在基类中使用virtual关键字声明的成员函数,它可以在派生类中被重新定义。当通过基类指针或引用调用虚函数时,程序会在运行时根据指针或引用实际指向的对象类型,来决定调用哪个类的虚函数版本,这就是所谓的动态绑定。
举个例子,假设有一个图形类Shape,它有一个绘制函数draw,而Circle和Rectangle是从Shape派生出来的具体图形类,它们都有自己独特的绘制方式 。代码如下:
class Shape {public:// 虚函数virtual void draw() const {std::cout << "绘制一个形状" << std::endl;}};class Circle : public Shape {public:// 重写虚函数void draw() const override {std::cout << "绘制一个圆形" << std::endl;}};class Rectangle : public Shape {public:// 重写虚函数void draw() const override {std::cout << "绘制一个矩形" << std::endl;}};void drawShape(const Shape& shape) {shape.draw();}int main() {Circle circle;Rectangle rectangle;drawShape(circle); // 输出 "绘制一个圆形"drawShape(rectangle); // 输出 "绘制一个矩形"return 0;}
在这个例子中,Shape类中的draw函数被声明为虚函数。Circle类和Rectangle类重写了draw函数,提供了各自的绘制逻辑 。在main函数中,我们定义了Circle和Rectangle的对象,并将它们传递给drawShape函数。drawShape函数接受一个Shape类型的引用,通过这个引用调用draw函数时,实际调用的是传入对象的具体类型(Circle或Rectangle)的draw函数版本,这就是虚函数实现运行时多态的过程。
对比一下普通函数和虚函数的调用。如果Shape类中的draw函数不是虚函数,那么通过Shape类型的指针或引用调用draw函数时,无论指针或引用实际指向的是哪个派生类对象,都会调用Shape类中的draw函数版本 ,这就无法实现多态效果。例如:
class Shape {public:// 普通函数void draw() const {std::cout << "绘制一个形状" << std::endl;}};class Circle : public Shape {public:// 这个函数与基类的draw函数不是重写关系,而是隐藏void draw() const {std::cout << "绘制一个圆形" << std::endl;}};class Rectangle : public Shape {public:// 这个函数与基类的draw函数不是重写关系,而是隐藏void draw() const {std::cout << "绘制一个矩形" << std::endl;}};void drawShape(const Shape& shape) {shape.draw();}int main() {Circle circle;Rectangle rectangle;drawShape(circle); // 输出 "绘制一个形状"drawShape(rectangle); // 输出 "绘制一个形状"return 0;}
3.1.虚函数重写规则
在 C++ 中,虚函数的重写需要遵循一定的规则,以确保多态行为的正确实现:
(1)函数签名一致
派生类中重写的虚函数必须与基类中的虚函数具有相同的函数名、参数列表(参数类型、个数和顺序)以及返回值类型 (除了协变返回类型的特殊情况,稍后会介绍)。例如:
class Base {public:virtual void func(int a) {std::cout << "Base::func(int)" << std::endl;}};class Derived : public Base {public:// 正确重写,函数名、参数列表相同void func(int a) override {std::cout << "Derived::func(int)" << std::endl;}};
在这个例子中,Derived类中的func函数与Base类中的func函数具有相同的函数名和参数列表,因此是正确的重写。
(2)协变返回类型
在 C++11 及以后的标准中,如果派生类重写的虚函数返回的是基类中虚函数返回类型的派生类类型,这种情况是允许的,称为协变返回类型 。例如:
class BaseClass {public:virtual ~BaseClass() {}};class DerivedClass : public BaseClass {public:virtual ~DerivedClass() {}};class Base {public:virtual BaseClass* clone() {return new Base();}};class Derived : public Base {public:// 协变返回类型,返回DerivedClass* 是BaseClass* 的派生类类型DerivedClass* clone() override {return new Derived();}};
在这个例子中,Base类的clone函数返回BaseClass*,Derived类重写的clone函数返回DerivedClass*,DerivedClass是BaseClass的派生类,这种重写是合法的。
(3)析构函数的重写
当基类的析构函数被声明为虚函数时,派生类的析构函数会自动重写基类的析构函数,即使它们的函数名不同 (因为析构函数的名字是固定的,与类名相关)。这一点非常重要,因为当通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,可能会导致派生类的析构函数无法被正确调用,从而产生内存泄漏等问题 。例如:
class Base {public:// 虚析构函数virtual ~Base() {std::cout << "Base析构函数" << std::endl;}};class Derived : public Base {public:~Derived() override {std::cout << "Derived析构函数" << std::endl;}};int main() {Base* basePtr = new Derived();delete basePtr; // 会先调用Derived的析构函数,再调用Base的析构函数return 0;}
在这个例子中,Base类的析构函数被声明为虚函数,当通过Base指针删除Derived对象时,会先调用Derived的析构函数,再调用Base的析构函数,确保资源的正确释放。如果Base的析构函数不是虚函数,那么delete basePtr时只会调用Base的析构函数,Derived的析构函数不会被调用,可能会导致Derived对象中分配的资源无法释放。
3.2.虚函数表与虚指针
为了实现虚函数的动态绑定,C++ 引入了虚函数表(vtable)和虚指针(vptr)的概念 。
虚函数表:每个包含虚函数的类都有一个虚函数表,它是一个函数指针数组,存储了该类中所有虚函数的地址 。虚函数表是在编译时生成的,并且对于每个类来说是唯一的。当一个类从另一个包含虚函数的类继承时,它会继承基类的虚函数表,并根据需要更新虚函数表中的条目(如果派生类重写了基类的虚函数) 。
虚指针:每个包含虚函数的类的对象都有一个虚指针,它指向该类的虚函数表 。虚指针通常位于对象内存布局的开头(在不同编译器实现中可能略有不同,但一般都在对象头部),它是对象的一部分,用于在运行时找到对象所属类的虚函数表,从而实现虚函数的动态调用 。
下面通过一个示例来更直观地理解虚函数表和虚指针的工作原理 :
class Base {public:virtual void func1() {std::cout << "Base::func1" << std::endl;}virtual void func2() {std::cout << "Base::func2" << std::endl;}};class Derived : public Base {public:void func1() override {std::cout << "Derived::func1" << std::endl;}};int main() {Base* basePtr = new Derived();// 获取对象的虚指针,假设虚指针位于对象内存布局的开头auto vptr = *(reinterpret_cast<void**>(basePtr));// 获取虚函数表中func1函数的指针auto func1Ptr = *reinterpret_cast<void***>(vptr);// 调用func1函数auto func1 = reinterpret_cast<void(*)()>(func1Ptr);func1();delete basePtr;return 0;}
Base类包含两个虚函数func1和func2,编译器会为Base类生成一个虚函数表,并在Base类的对象中添加一个虚指针,指向这个虚函数表。
当Derived类继承Base类并重写func1函数时,Derived类会继承Base类的虚函数表,并将虚函数表中func1函数的指针更新为指向Derived::func1的地址 。
在main函数中,创建了一个Derived类的对象,并通过Base指针指向它。通过获取对象的虚指针,进而获取虚函数表中func1函数的指针,最后调用func1函数 。由于Derived类重写了func1函数,所以实际调用的是Derived::func1,这就展示了虚函数表和虚指针如何在运行时实现多态调用 。
需要注意的是,上述通过指针直接操作虚函数表和虚指针的代码只是为了演示原理,在实际编程中,我们通常不会这样做,而是直接使用虚函数的语法来实现多态,这样更安全、更易读 。虚函数表和虚指针是 C++ 编译器内部实现多态的机制,了解它们的工作原理有助于我们更好地理解运行时多态的实现过程,但在日常开发中,我们更多地是利用虚函数带来的多态特性来编写灵活、可扩展的代码 。
4.多态性练习—图形绘制系统
在图形绘制系统中,多态性有着广泛的应用。我们可以定义一个抽象的图形基类Shape,并在其中声明一个纯虚函数draw 。这个纯虚函数为所有派生的具体图形类提供了一个统一的绘制接口。例如:
class Shape {public:// 纯虚函数,没有具体实现,要求派生类必须重写virtual void draw() const = 0;};
然后,从Shape类派生出不同的具体图形类,如Circle(圆形)、Rectangle(矩形)等,并在各自的派生类中实现draw函数 ,来提供具体的绘制逻辑 。
class Circle : public Shape {public:void draw() const override {std::cout << "绘制一个圆形" << std::endl;}};class Rectangle : public Shape {public:void draw() const override {std::cout << "绘制一个矩形" << std::endl;}};
在实际绘制图形时,我们可以使用Shape类型的指针或引用来存储不同类型的图形对象 。当调用draw函数时,根据对象的实际类型,会自动调用相应派生类中的draw函数实现 。这样,我们就可以通过一个统一的接口来绘制各种不同的图形,而不需要为每种图形编写单独的绘制函数 。例如:
void drawShapes(const Shape* shapes[], int count) {for (int i = 0; i < count; ++i) {shapes[i]->draw();}}int main() {Shape* shapes[2];shapes[0] = new Circle();shapes[1] = new Rectangle();drawShapes(shapes, 2);// 释放内存for (int i = 0; i < 2; ++i) {delete shapes[i];}return 0;}
在这个例子中,drawShapes函数接受一个Shape指针数组和数组的大小,通过循环调用每个Shape对象的draw函数,实现了对不同类型图形的统一绘制 。当需要添加新的图形类型时,只需要从Shape基类派生新的类,并实现draw函数,而不需要修改现有的绘制代码,这大大提高了系统的可扩展性和灵活性 。
5.多态性练习—游戏开发
在游戏开发领域,多态性同样发挥着重要作用。以角色扮演游戏(RPG)为例,游戏中有各种不同类型的角色,如战士(Warrior)、法师(Mage)、弓箭手(Archer)等 ,每个角色都有自己独特的行为和属性 。我们可以定义一个抽象的角色基类Character,并在其中声明一些虚函数,如attack(攻击)、move(移动)等 ,来描述角色的基本行为。
class Character {public:virtual void attack() const = 0;virtual void move() const = 0;};
然后,从Character类派生出不同的具体角色类,并在每个派生类中实现这些虚函数,以体现不同角色的行为差异 。
class Warrior : public Character {public:void attack() const override {std::cout << "战士挥舞大剑进行攻击" << std::endl;}void move() const override {std::cout << "战士快速冲向敌人" << std::endl;}};class Mage : public Character {public:void attack() const override {std::cout << "法师释放魔法进行攻击" << std::endl;}void move() const override {std::cout << "法师瞬移到安全位置" << std::endl;}};class Archer : public Character {public:void attack() const override {std::cout << "弓箭手射出利箭进行攻击" << std::endl;}
在游戏循环中,我们可以使用Character类型的指针或引用来操作不同类型的角色 。当角色执行攻击或移动等操作时,会根据角色的实际类型调用相应派生类中的函数实现 。这样,游戏引擎就可以通过一个统一的接口来管理和控制各种不同类型的角色,使得代码更加简洁、灵活,易于维护和扩展 。例如:
void playGame(Character* characters[], int count) {for (int i = 0; i < count; ++i) {characters[i]->attack();characters[i]->move();}}int main() {Character* characters[3];characters[0] = new Warrior();characters[1] = new Mage();characters[2] = new Archer();playGame(characters, 3);// 释放内存for (int i = 0; i < 3; ++i) {delete characters[i];}return 0;}
在这个例子中,playGame函数模拟了游戏的运行过程,通过调用每个Character对象的attack和move函数,实现了不同角色的攻击和移动行为 。如果后续需要添加新的角色类型,只需要从Character基类派生新的类,并实现相应的虚函数,而游戏的核心逻辑(如游戏循环、角色管理等)无需大幅修改,这充分展示了多态性在游戏开发中的优势 。
- 上一篇:必须掌握的数据核对技巧,收藏备用
- 下一篇:检查文本是否相同EXACT函数应用技巧
相关推荐
- 苹果笔记本怎么下载windows系统
-
方法一:使用BootCamp方法二:使用虚拟机方法三:使用Wine简介BootCamp是苹果电脑自带的一个软件,可以帮助用户在Mac上安装Windows操作系统。虚拟机则是运行在Mac上的一个软件...
- 华硕电脑激活码(华硕电脑windows激活码在哪里)
-
你所说的应该是系统激活密钥吧华硕OEM密钥就行!HomePremium(家庭高级版):27GBM-Y4QQC-JKHXW-D9W83-FJQKDUltimate(旗舰版):6K2KY-BF...
- wifi如何防止别人蹭网(wifi如何防止别人蹭网呢)
-
防止WIFI被蹭网的方法1、家庭的wifi信号,一般是通过设置无线路由器发射出来。在设置无线路由器时,一定要注意设置无线密码的安全强度。最好含有大小写字母加数字的组合,不要设置连续数字,比如“1234...
- 无线ap安装示意图(无线ap如何安装方法)
-
商用无线ap安装完成后连接ap应用添加wifi网络信号将无线网卡插入电脑USBWindows7/8系统下,网卡安装成功后直接进行软件安装...2.继续安装类型,推荐保持默认选择,点击“下一步”继续安装...
- miui官网开发版下载(小米miui开发版下载)
-
你好,miui12开发版下载方式如下1打开浏览器,登录小米Rom官网,2进去之后搜索自己的手机型号,3点击进入就可以看到所有发布过的miui版本4点击想要的12版本下载即可。现在的MIUI开发版需要申...
-
- 惠普台式机进入bios设置u盘启动
-
设置u盘启动的步骤如下:1、首先,将u盘插入hp台式机的USB接口处。2、开机快速断续的按F10键进入BIOS设置界面。3、将光标移到【BootDevicePriority】选项按回车键进入。4、选择【HDDGroupBootPr...
-
2026-01-15 00:37 liuian
- 云手机免费版无限挂机怎么用
-
1、登陆后,如果需要挂网页游戏,点击服务器的左下角,找到IE浏览器,然后打开网页游戏,登陆你的账号就行了,不要关闭IE浏览器,你的网页游戏就会24小时挂在云服务器上面。2、如果想要挂机,打开IE浏览器...
- 上海最近3天疫情情况(上海近几天的新冠疫情情况)
-
根据国家卫健委的每天疫情通报及上海市的疫情通报,上海没有一个区属中高风险地区,所以从上海任何一个区返乡都不需要隔离14天。上海这么大的城市,每天人来人往的Ill流不息,能继续做到区级地区没有中高级风险...
- windows media player怎么下载
-
方法如下:在安装WMP11时只是把C:\DocumentsandSettings\AllUsers\ApplicationData\WindowsGenuineAdvantage\data...
- during(during用法)
-
during用来表示一段时间,其意义大致相当于in的用法。一般来说,凡是能用in的地方,也可以用during.例如:Hecametoseemeduringmyabsence.Don’t...
- 深圳电脑城在哪里(深圳电脑卖场)
-
龙岗:世纪电脑城,平湖电脑城,京科电脑城坪山新区:坪山电脑城龙华:观澜电脑城,大浪电脑城,宏华电脑城,龙华电子城宝安区:赛格电子城,宝安电子城,丰明电脑城,沙井电子城龙岗中心区那边有两个电子城,...
- 电脑上怎么清理c盘垃圾(电脑里怎么清理c盘的东西)
-
C:\ProgramFiles\WindowsApps(隐藏文件夹)。打开“此电脑”,点击“查看”,勾选“隐藏的项目”,即可查看隐藏文件。为保证文件安全,此文件夹需要获取权限才能操作。获取方式...
- 手机哪个杀毒软件最好用
-
杀毒软件我有用过好几种用过之觉得体验感及安全性来说人喜欢推荐腾讯手机管家功能比较全面监控流量、查杀病毒、保护隐私等等界面也比较漂亮重点还要定期扫描同时也要轻易点开别人发链接之类软件有提示危险绝对要点开...
-
- 笔记本电脑怎样截图(苹果笔记本电脑怎样截图)
-
方法/步骤1第一个办法自然是我们最常见最简单的,使用“PrintScreen”键截图了。点击“PrintScreen”键,我们就可以直接截取全部屏幕,找个对话框或者文字区域粘贴就好了。我截的图是这样的2Windows系统都自带有截图工具,我...
-
2026-01-14 22:37 liuian
- vaio笔记本u盘启动(hipaa笔记本u盘启动)
-
可能是u盘启动快捷键没有使用正确。因为笔记本型号不同,所以BIOS会有所不同,并且进入bios的启动快捷键也会不同。而索尼笔记本开机需要按F2键进入bios设置中。 2、在bios中没有正确设置u盘...
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
