C++第14课:C++类的多态性(c++的多态性是指)
liuian 2025-07-28 18:13 43 浏览
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函数应用技巧
相关推荐
- 搭建一个20人的办公网络(适用于20多人的小型办公网络环境)
-
楼主有5台机上网,则需要一个8口路由器,组网方法如下:设备:1、8口路由器一台,其中8口为LAN(局域网)端口,一个WAN(广域网)端口,价格100--400元2、网线N米,这个你自己会看了:)...
- 笔记本电脑各种参数介绍(笔记本电脑各项参数新手普及知识)
-
1、CPU:这个主要取决于频率和二级缓存,频率越高、二级缓存越大,速度越快,现在的CPU有三级缓存、四级缓存等,都影响相应速度。2、内存:内存的存取速度取决于接口、颗粒数量多少与储存大小,一般来说,内...
- 汉字上面带拼音输入法下载(字上面带拼音的输入法是哪个)
-
使用手机上的拼音输入法打成汉字的方法如下:1.打开手机上的拼音输入法,在输入框中输入汉字的拼音,例如“nihao”。2.根据输入法提示的候选词,选择正确的汉字。例如,如果输入“nihao”,输...
- xpsp3安装版系统下载(windowsxpsp3安装教程)
-
xpsp3纯净版在采用微软封装部署技术的基础上,结合作者的实际工作经验,融合了许多实用的功能。它通过一键分区、一键装系统、自动装驱动、一键设定分辨率,一键填IP,一键Ghost备份(恢复)等一系列...
- 没有备份的手机数据怎么恢复
-
手机没有备份恢复数据方法如下1、使用数据线将手机与电脑连接好,在“我的电脑”中可以看到手机的盘符。 2、将手机开启USB调试模式。在手机设置中找到开发者选项,然后点击“开启USB调试模式”。 3、...
- 电脑怎么激活windows11专业版
-
win11专业版激活方法有多种,以下提供两种常用的激活方式:方法一:使用激活密钥激活。在win11桌面上右键点击“此电脑”,选择“属性”选项。进入属性页面后,点击“更改产品密钥或升级windows”。...
- 华为手机助手下载官网(华为手机助手app下载专区)
-
华为手机助手策略调整,已不支持从应用市场下载手机助手,目前华为手机助手是需要在电脑上下载或更新手机助手到最新版本,https://consumer.huawei.com/cn/support/his...
- 光纤线断了怎么接(宽带光纤线断了怎么接)
-
宽带光纤线断了可以重接,具体操作方法如下:1、光纤连接的时候要根据束管内,同色相连,同芯相连,按顺序进行连接,由大到小。一般有三种连接方法,分别是熔接、活动连接和机械连接。2、连接的时候要开剥光缆,抛...
- win7旗舰版和专业版区别(win7旗舰版跟专业版)
-
1、功能区别:Win7旗舰版比专业版多了三个功能,分别是Bitlocker、BitlockerToGo和多语言界面; 2、用途区别:旗舰版的功能是所有版本中最全最强大的,占用的系统资源,...
- 万能连接钥匙(万能wifi连接钥匙下载)
-
1、首先打开wifi万能钥匙软件,若手机没有开启WLAN,就根据软件提示打开WLAN开关;2、打开WLAN开关后,会显示附近的WiFi,如果知道密码,可点击相应WiFi后点击‘输入密码’连接;3、若不...
- 雨林木风音乐叫什么(雨林木风是啥)
-
雨林木风的创始人是陈年鑫先生。陈年鑫先生于1999年创立了雨林木风公司,其初衷是为满足中国市场对高品质、高性能电脑的需求。在陈年鑫先生的领导下,雨林木风以技术创新、产品质量和客户服务为核心价值,不断推...
- aics6序列号永久序列号(aics6破解序列号)
-
关于AICS6这个版本,虽然是比较久远的版本,但是在功能上也是十分全面和强大的,作为一名平面设计师的话,AICS6的现有的功能已经能够应付几乎所有的设计工作了……到底AICC2019的功能是不是...
- 手机可以装电脑系统吗(手机可以装电脑系统吗怎么装)
-
答题公式1:手机可以通过数据线或无线连接的方式给电脑装系统。手机安装系统需要一定的技巧和软件支持,一般需要通过数据线或无线连接的方式与电脑连接,并下载相应的软件和系统文件进行安装。对于大部分手机用户来...
- 一周热门
- 最近发表
- 标签列表
-
- 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)
