王牌飞行员带你彻底搞懂C++的虚函数和纯虚函数

2023-11-21 20:40

本文主要是介绍王牌飞行员带你彻底搞懂C++的虚函数和纯虚函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

首先,我们要搞清楚女神的所作所为,即语法规范。然后再去探究她背后的逻辑道理。她的语法说来也不复杂,概括起来就这么几条:

1.在类成员方法的声明(不是定义)语句前面加个单词:virtual,她就会摇身一变成为虚函数;
  2.在虚函数的声明语句末尾中加个 =0 ,她就会摇身一变成为纯虚函数;
  3.子类可以重新定义基类的虚函数,我们把这个行为称之为复写(override);
  4.不管是虚函数还是纯虚函数,基类都可以为他们提供实现(implementation),如果有的话子类可以调用基类的这些实现;
  5.子类可自主选择是否要提供一份属于自己的个性化虚函数实现;
  6.子类必须提供一份属于自己的个性化纯虚函数实现。

纯虚函数
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。而针对每种动物的方法又有所不同,此时需要使用多态特性,也就需要在基类中定义虚函数。
纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。
定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
含有纯虚函数的类称之为抽象类,它不能生成对象(创建实例),只能创建它的派生类的实例。抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
虚函数 vs 纯虚函数,如何选用?
1. 当基类中的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供缺省备选方案的时候,该方法应该设计为虚函数。
2. 当基类中的某个成员方法,必须由子类提供个性化实现的时候,应该设计为纯虚函数。

首先,飞行器。由于我们假设所有的飞行器都有两种行为:加油和飞行。因此我们可以将这两种行为抽象到一个基类中,并由它来派生具体的某款飞行器。

 // 这是一个描述飞行器的基类,提供了两个基本的功能:加油和飞行。class aircraft {virtual void refuel();  // 加燃油,普通虚函数virtual void fly()=0;   // 飞行,纯虚函数};
 // 这是一个普通虚函数,意味着基类希望子类提供自己的个性化实现代码,但基类// 同时也提供一个缺省的虚函数实现版本,在子类不复写该虚函数的情况下作为备选方案。void aircraft::refuel() {// 加通用型燃油std::cout << "加通用燃油" << std::endl;}
 // 这是一个纯虚函数,意味着基类强制子类必须提供自己的个性化版本,否则编译将失败。void aircraft::fly() {// 一种不应该被使用的缺省飞行方案std::cout << "一种不应该被使用的缺省飞行方案" << std::endl;}

但让人惊奇的是,C++仍然保留了基类提供该纯虚函数代码实现的权利,这也许是给千变万化的实际情况留下后路。

有了基类aircraft,我们就可以潇洒地派生出各式各样的飞行器了,比如轰炸机和直升机:

  // 轰炸机类定义,复写了加油和飞行class bomber : public aircraft {void refuel() {}  // 加轰炸机的特殊燃油void fly() {}     // 轰炸机实弹飞行};// 直升机类定义,复写了飞行代码,但没有复写加油class copter : public aircraft {void fly() {}  // 直升机盘旋};

以上代码可以看到,直升机类(copter)没有自己的加油方式,直接使用了基类提供的缺省加油的方式。此时我们来定义一个能驾驭多机型的王牌飞行员类:

 // 一个能驾驶各种飞行器的王牌飞行员class pilot {void refuelPlane(aircraft *p);void dirvePlane(aircraft *p);};
// 给我什么飞机我就加什么油void pilot::refuelPlane(aircraft *p) {p->refuel();}// 给我什么飞机我就怎么飞void pilot::dirvePlane(aircraft *p) {p->fly();}

很明显,我们接下来要给这位很浪的飞行员表演一下操纵各种飞行器的机会,我们来定义各种飞机然后丢给他去处理。

 // 定义两架飞机,一架轰6K,一架武直10aircraft *H6K = new bomber;aircraft *WZ10 = new copter;// 来一个王牌飞行员,给H6K加油(加的是轰炸机特殊燃油),并且按照H6K的特点飞行pilot Jack;Jack.refuelPlane(H6K);  // 加轰炸机燃油Jack.flyPlane(H6K);     // 轰炸机实弹飞行// 给WZ10加油(加的是基类提供的通用燃油),按照WZ10的特点飞行Jack.refuelPlane(WZ10);  // 加通用型燃油Jack.flyPlane(WZ10);     // 直升机盘旋

上述代码体现了最经典的所谓多态的场景,给Jack不同的飞机,就能表现不同的结果。虚函数和纯虚函数都能做到这一点,区别是,子类如果不提供虚函数的实现,那就会自动调用基类的缺省方案。而子类如果不提供纯虚函数的实现,则编译将会失败。基类提供的纯虚函数实现版本,无法通过指向子类对象的基类类型指针或引用来调用,因此不能作为子类相应虚函数的备选方案。

下面给出总结:

1.当基类的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供一个备选方案的时候,请将其设计为虚函数。例如飞行器的加油动作,每种不同的飞行器原则上都应该有自己的个性化的加充燃油的方式,但也不免可以有一种通用的燃油加充方式;
  2.当基类的某个成员方法,必须由子类提供个性化实现的时候,请将其设计为纯虚函数。例如飞行器的飞行动作,逻辑上每种飞行器都必须提供为其特殊设计的个性化飞行行为,而不应该有任何一种"通用的飞行方式";
  3.使用一个基类类型的指针或者引用,来指向子类对象,进而调用经由子类复写了的个性化的虚函数,这是C++实现多态性的一个最经典的场景;
  4.基类提供的纯虚函数的实现版本,并非为了多态性考虑,因为指向子类对象的基类指针和引用无法调用该版本。纯虚函数在基类中的实现跟多态性无关,它只是提供了一种语法上的便利,在变化多端的应用场景中留有后路;
  5.虚函数和普通的函数实际上是存储在不同的区域的,虚函数所在的区域是可被覆盖(也称复写override)的,每当子类定义相同名称的虚函数时就将原来基类的版本给覆盖了,另一侧面也说明了为什么基类中声明的虚函数在后代类中不需要另加声明一律自动为虚函数,因为它所存储的位置不会发生改变。而普通函数的存储区域不会覆盖,每个类都有自己独立的区域互不相干。
  在这里插入图片描述

这篇关于王牌飞行员带你彻底搞懂C++的虚函数和纯虚函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/404980

相关文章

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

一文带你搞懂Python中__init__.py到底是什么

《一文带你搞懂Python中__init__.py到底是什么》朋友们,今天我们来聊聊Python里一个低调却至关重要的文件——__init__.py,有些人可能听说过它是“包的标志”,也有人觉得它“没... 目录先搞懂 python 模块(module)Python 包(package)是啥?那么 __in

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

如何将Python彻底卸载的三种方法

《如何将Python彻底卸载的三种方法》通常我们在一些软件的使用上有碰壁,第一反应就是卸载重装,所以有小伙伴就问我Python怎么卸载才能彻底卸载干净,今天这篇文章,小编就来教大家如何彻底卸载Pyth... 目录软件卸载①方法:②方法:③方法:清理相关文件夹软件卸载①方法:首先,在安装python时,下

shell编程之函数与数组的使用详解

《shell编程之函数与数组的使用详解》:本文主要介绍shell编程之函数与数组的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录shell函数函数的用法俩个数求和系统资源监控并报警函数函数变量的作用范围函数的参数递归函数shell数组获取数组的长度读取某下的