C++面向对象:virtual关键字的使用

virtual实现多态

类的多态特性是支持面向对象的语言最主要的特性,但是支持类并不能说明就是支持面向对象,能够解决多态问题的语言,才是真正支持面向对象的开发的语言。
C++多态举例:

#include <iostream>  
using namespace std;  class Vehicle {  
public:  Vehicle(float speed, int total) : speed(speed), total(total) {}  virtual void ShowMember() { // 声明为虚函数  cout << "Vehicle function ShowMember." << endl;  }  
protected:  float speed;  int total;  
};  class Car : public Vehicle {  
public:  Car(int aird, float speed, int total) : Vehicle(speed, total), aird(aird) {}  void ShowMember() override { // 使用override关键字,明确表示这是重写基类的虚函数  
//虚函数在派生类中,由于继承的关系,这里的virtual也可以不加cout << "Car function ShowMember." << endl;  }  
protected:  int aird;  
};  void test(Vehicle &temp) {  temp.ShowMember(); // 这里会根据temp的实际类型调用对应版本的ShowMember函数  
}  int main() {  Vehicle a(120, 4);  Car b(180, 110, 4);  test(a); // 输出: Vehicle function ShowMember.  test(b); // 输出: Car function ShowMember.  return 0;  
}

代码展示C++中多态性的一个基本用法:

通过基类指针或引用调用派生类对象的虚函数时,将执行派生类中覆盖该函数的版本。

程序在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性.

 多态性依赖虚函数的定义,在需要解决多态问题的重载成员函数前,加上virtual关键字,成员函数就变成了虚函数。但是虚函数增加了一些数据存储和执行指令的开销。

用基类指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。通过基类指针或基类引用做形参,当实参传入不同的派生类(或基类)的指针或引用,在函数内部触发动态绑定,从而来运行时 实现多态的。

这种技术可以让父类的指针有“多种形态”,本质上一种泛型技术,就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

虚函数的定义遵循的规则: 

  1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。 

  2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。 

  3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。 

  4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义定义,但是在编译的时候系统仍然将它看做是非内联的。 

  5.构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。 

  6.析构函数可以是虚函数,而且通常声明为虚函数。 虽然使用虚函数会降低效率,但是在处理器速度越来越快时,将一个类中的所有成员函数都定义成为virtual总是有好处的,它除了会增加一些额外的开销是没有其它坏处的,对于保证类的封装特性是有好处的。

虚函数表

虚函数是通过一张虚函数表实现。表中是类的虚函数的地址表,可以解决继承、覆盖的问题。

有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以当用父类的指针来操作一个子类的时候,它像一个地图一样,指明了实际所应该调用的函数。C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。所以可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

假设有这样的一个类:

 class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }
}

按照上面的说法,可以通过Base的实例来得到虚函数表。 

  typedef void(*Fun)(void); Base b;Fun pFun = NULL;cout << "虚函数表地址:" << (int*)(&b) << endl;cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;// Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&b));pFun();

通过这个示例,我们可以看到,我们可以通过强行把&b转成int ,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:

(Fun)*((int*)*(int*)(&b)+0);  // Base::f()
(Fun)*((int*)*(int*)(&b)+1);  // Base::g()
(Fun)*((int*)*(int*)(&b)+2);  // Base::h()

注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。

下面将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。

一般继承(无虚函数覆盖)

假设有如下所示的一个继承关系:


在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示。对于实例Derive d 的虚函数表如下:

 可以看到

1)虚函数按照其声明顺序放于表中。 
2)父类的虚函数在子类的虚函数前面。

一般继承(有虚函数覆盖)

如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

为了让大家看到被继承过后的效果,在这个类的设计中,只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

从表中可以看到下面几点,

(1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。 
(2)没有被覆盖的函数依旧。

这样,我们就可以看到对于下面这样的程序,

Base *b = new Derive();

b->f();

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

多重继承(无虚函数覆盖)

下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

对于子类实例中的虚函数表,是下面这个样子:

可以看到:

1)  每个父类都有自己的虚表。 
2)  子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)1

这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

多重继承(有虚函数覆盖)

下面我们再来看看,如果发生虚函数覆盖的情况。

下图中,我们在子类中覆盖了父类的f()函数。

下面是对于子类实例中的虚函数表的图:

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

 Derive d;Base1 *b1 = &d;Base2 *b2 = &d;Base3 *b3 = &d;b1->f(); //Derive::f()b2->f(); //Derive::f()b3->f(); //Derive::f()b1->g(); //Base1::g()b2->g(); //Base2::g()b3->g(); //Base3::g()

为什么具备多态特性的类的析构函数,有必要声明为virtual?

将基类的析构函数声明为virtual,那么当通过基类指针删除对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数。这样就可以确保所有的清理工作都被正确执行,避免了资源泄漏。因此,如果类被设计为基类,并且希望它的派生类能够通过基类指针被正确地删除,那么应该将基类的析构函数声明为virtual。

纯虚函数

纯虚函数是一种特殊的虚函数,它在基类中声明为= 0,表明在基类中不提供具体的实现,并且要求任何派生类都必须提供该函数的实现。

纯虚函数为派生类提供了一种接口规范,确保派生类必须实现特定的功能。

如果一个类包含至少一个纯虚函数,那么这个类就是一个抽象基类。抽象基类不能被实例化,因为它包含不完整的功能。

派生类必须重写纯虚函数才能被实例化、如果一个类从抽象基类派生,并且它没有重写所有的纯虚函数,那么这个派生类也仍然是抽象的,不能实例化。派生类必须完全遵循抽象基类定义的接口规范。

#include <iostream>  
using namespace std;  // 抽象基类  
class CVirtual {  
public:  CVirtual() {}  virtual ~CVirtual() {  cout << " CVirtual destruction " << endl;  }  // 纯虚函数  virtual void fun() = 0;  
};  // 派生类  
class CDerived : public CVirtual {  
public:  CDerived() {}  ~CDerived() {}  // 覆盖纯虚函数  void fun() override {  cout << " CDerived function call " << endl;  }  
};  int main() {  // CVirtual v; // 错误!抽象基类不能被实例化  CDerived d; // 正确!派生类提供了纯虚函数的实现  d.fun(); // 调用派生类中的fun()实现  return 0;  
}

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

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

相关文章

2404-mysql数据库笔记

DataBase-数据库 mariadbmysql https://doc.canglaoshi.org/ 一、SQL&#xff08;Structured Query Language&#xff09;结构化查询语言 1、用于访问和处理数据库的标准计算机语言 2、语法特点 &#xff08;1&#xff09;SQL对关键字的大小写不敏感(windows) &#xff08;…

AWS中所有与数据科学有关的服务一览表(MLS-C01)

本表包括AWS Global中的服务&#xff0c;部分服务在AWS China中还没有。 Amazon Sagemaker数据科学家的开发平台&#xff0c;含多个组件用于整个机器学习模型开发的各个阶段。 Amazon Transcribe 语音转文字&#xff0c;即ASR Amazon Translate 翻译 Amazon Textract 从PD…

蓝桥杯算法基础(24):多维数组与矩阵(4道小题)java版

题1:顺时针打印二维数组 static void print(int[][] matrix){int leftUpRow0,leftUpCol0,rightDownRowmatrix.length-1,rightDownColmatrix[0].length-1;while(leftUpRow<rightUpRow&&leftUpCol<rightDownCol){//在不是方阵的情况下&#xff0c;是扁平的时&#…

鼠标右键增加CMD打开快捷键(亲测有效)

1. 我们用regedit或者其他注册表编辑器定位到HKEY_CLASSES_ROOT\Directory\Background\shell\处&#xff0c;右击新建项“OpenCMDHere”&#xff0c;并在该项下&#xff0c;右击新建项“command”。 2.我们在右边OpenCMDHere项下&#xff0c;右击新建REG_DWORD类型整数值。设置…

AWS监控,AWS 性能监控工具

监控云部署的性能是 IT 环境正常运行的内在条件。AWS 云是一个架构良好的框架&#xff0c;管理员可以使用专用的AWS 性能监控工具增强服务的功能。执行AWS监视是为了跟踪在AWS环境中积极运行的应用程序工作负载和资源。AWS监视器跟踪各种AWS云指标&#xff0c;以帮助提高在其上…

Elasticsearch:全文搜索的利器

1. 简介 Elasticsearch是一个基于Lucene的分布式搜索引擎&#xff0c;能够支持准实时的数据检索NRT(near real-time),支持海量数据的处理&#xff0c;包括结构化和非结构化数据&#xff0c;提供强大的全文搜索能力&#xff0c;但是ES不仅仅是一个全文搜索引擎&#xff0c;他能…

递归算法c++

主页:(*∇&#xff40;*) 咦,又好了~ xiaocr_blog 算法概述&#xff1a;递归算法是一种直接或者间接调用自身函数或者方法的算法。说简单了就是程序自身的调用。 算法实质&#xff1a;递归算法就是将原问题不断分解为规模缩小的子问题&#xff0c;然后递归调用方法来表示问题的…

【小白笔记:JetsonNano学习(二)JetsonNano 安装开机问题屏幕进不去】

重新烧录sd卡后插入Jetson Nano后出现的界面显示烧录失败&#xff0c;如下所示&#xff1a; 将经过烧录之后的sd卡插入jetson nano之后出现以下的几个界面&#xff0c;表示烧录失败。 原因分析&#xff1a;烧录的tf卡为sd卡时候的格式化的格式不对&#xff0c;新建格式出错&am…

LeetCode 222.完全二叉树的节点个数

给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树 的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面一层的节点都集中在该层最左边的若干位置。若最…

Hack The Box-Analytics

目录 信息收集 namp whatweb WEB 信息收集 feroxbuster RCE漏洞 提权 get user get root 信息收集 namp 端口信息探测┌──(root㉿ru)-[~/kali/hackthebox] └─# nmap -p- 10.10.11.233 --min-rate 10000 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-03-…

python adb脚本

Python调用adb shell ls&#xff0c;取前5个字符存放&#xff0c;并打印出来 import subprocess def run_adb_shell_command(command):# 设置adb的路径&#xff0c;根据您的实际情况进行修改 adb_path adb # 使用subprocess运行adb shell命令&#xff0c;并捕获输出 resu…

Python面向对象——架构设计【2】

练习1&#xff1a;打电话 请使用面向对象思想描述下列情景: 小明使用手机打电话,还有可能使用座机.... class People:def __init__(self,name):self.name namedef call_up(self,tool):print(self.name,end"")tool.call()class Tools:def __init__(self,way):self.wa…

OpenCV(八)——基本线条操作

基本线条操作 OpenCV中提供了基本的线条的操作&#xff0c;包括画直线、画矩形、画圆形等。 &#xff08;1&#xff09;画直线&#xff0c;在OpenCV中利用line()画直线&#xff0c;形式为image_with_line cv2.line(image, start_point, end_point, color, thickness)。line(…

智慧矿山新趋势:大数据解决方案一览

1. 背景 随着信息技术的快速发展和矿山管理需求的日益迫切&#xff0c;智慧矿山作为一种创新的矿山管理方式应运而生。智慧矿山借助先进的信息技术&#xff0c;实现对矿山生产、管理、安全等各方面的智能化、高效化、协同化&#xff0c;是矿山行业转型升级的必然趋势。 欢迎关…

Java 容器都有哪些?

Java容器是Java编程语言中用于存储和组织对象的数据结构。在Java中&#xff0c;容器类库提供了各种类型的容器&#xff0c;每种容器都有其特定的用途和适用场景。 1. 数组&#xff08;Array&#xff09; 数组是Java中最基本的容器类型之一&#xff0c;用于存储相同数据类型的…

【LabVIEW FPGA入门】并行执行

利用图形化编程的并行特性以及 FPGA 上 LabVIEW 图的真正并行实现&#xff0c;您可以通过将应用程序代码划分为更小的进程来进一步优化执行速度。与整个应用程序在一个循环中运行相比&#xff0c;这使得每个进程能够实现更高的循环速率和更高的应用程序整体执行速率。 …

Java语法学习八之认识String类

String类的重要性 在C语言中已经涉及到字符串了&#xff0c;但是在C语言中要表示字符串只能使用字符数组或者字符指针&#xff0c;可以使用标准库提供的字符串系列函数完成大部分操作&#xff0c;但是这种将数据和操作数据方法分离开的方式不符合面相对象的思想&#xff0c;而…

C++开发基础——函数模板

一&#xff0c;函数模板 1.基础概念 模板编程是C中泛型编程的基础。 一个模板可以是创建类或者函数的蓝图。 模板编程分两种&#xff0c;分别是算法抽象的模板、数据抽象的模板。算法抽象的模板以函数模板为主&#xff0c;数据抽象的模板以类模板为主。 基于函数模板生成的…

Vue面试题,背就完事了

1.vue的生命周期有哪些及每个生命周期做了什么? Vue.js 的生命周期可以分为以下几个核心阶段&#xff0c;每个阶段都伴随着特定的钩子函数&#xff08;生命周期钩子&#xff09;来执行相应的操作&#xff1a; 创建阶段&#xff1a; beforeCreate&#xff1a;实例被创建后、数…

13.Python从入门到精通—Python 集合操作与方法概览

13.Python从入门到精通—Python 集合操作与方法概览 Python 集合集合的基本操作1、添加元素2、移除元素3、计算集合元素个数4、清空集合5、判断元素是否在集合中存在 集合内置方法完整列表 Python 集合 在Python中&#xff0c;集合是一种无序、不重复的数据类型。集合通常用于…