C++面向对象程序设计 - 虚函数

        在C++中,虑函数(Virtual Function)是面向对象编程(OOP)中的一个重要概念,它允许派生类(或称为子类)覆盖基类(或称为父类)中的成员函数。当通过基类指针或引用调用一个虚函数时,如果指针或引用实际指向的是一个派生类对象,那么就会调用派生类的虚函数方法,而不是基类中的方法,这种机制被称为动态绑定或运行时绑定。

一、虚函数的作用

        C++中的虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

1.1 通过指针访问同族类对象

        示例代码:

#include <iostream>
#include <string>
using namespace std;
// 声明基类Student
class Student{protected:int num;string name;float score;public:Student(int num, string n, float s): num(num), name(n), score(s){}void display(){cout <<"num=" <<num <<", name=" <<name <<", score=" <<score <<endl;}
};
// 声明派生类Graduate
class Graduate: public Student{private:float wages;		//工资public:Graduate(int num, string n, float s, float w): Student(num, n, s), wages(w){}void display(){cout <<"num=" <<num <<", name=" <<name <<", score=" <<score <<", wages=" <<wages <<endl;}
};
int main(){// 创建类Student对象sStudent s(1001, "Wei Li", 90);// 创建类Graduate对象gGraduate g(1002, "Qiang Liu", 92, 5000.0);// 定义类Student指针变量,并指向对象sStudent *pt = &s;// 显示对象s结果pt->display();// 将指针指向研究生pt = &g;// 输出研究生的结果pt->display();return 0;
}

        输出结果如下图:

        以上案例虽然指针变量pt分别指向对象s和对象g,调用两个对象中display()函数,但实际指针变量指向的都是继承基类部分,所以两次输出都是执行基类中的display()函数,在输出研究生对象g的信息时,并未输出工资这一项恰当说明这一点。

1.2 通过对象直接调用

        如果输出各自类中的信息,可以通过各自对象直接调用,代码如下:

int main(){// 创建类Student对象sStudent s(1001, "Wei Li", 90);// 创建类Graduate对象gGraduate g(1002, "Qiang Liu", 92, 5000.0);s.display();		//输出学生对象s的信息g.display();		//输出研究生对象g的信息return 0;
}

        输出结果如下图:

1.3 多态性的体现

        除了用各自对象直接调用display()函数外,如何用指针变量输出也能得到上图效果呢?其实用虚函数就能解决这个问题了,现在在基类Student中定义display()函数前面加上virtual关键字即可。代码如下:

#include <iostream>
#include <string>
using namespace std;
// 声明基类Student
class Student{protected:int num;string name;float score;public:Student(int num, string n, float s): num(num), name(n), score(s){}virtual void display(){cout <<"num=" <<num <<", name=" <<name <<", score=" <<score <<endl;}
};
// 声明派生类Graduate
class Graduate: public Student{private:float wages;		//工资public:Graduate(int num, string n, float s, float w): Student(num, n, s), wages(w){}void display(){cout <<"num=" <<num <<", name=" <<name <<", score=" <<score <<", wages=" <<wages <<endl;}
};
int main(){// 创建类Student对象sStudent s(1001, "Wei Li", 90);// 创建类Graduate对象gGraduate g(1002, "Qiang Liu", 92, 5000.0);// 定义类Student指针变量,并指向对象sStudent *pt = &s;// 显示对象s结果pt->display();// 将指针指向研究生pt = &g;// 输出研究生的结果pt->display();return 0;
}

        从程序上来看,只是在Student类中display()函数前面加上了关键字virtual,其他部分都未改变;运行后输出结果可看出,指针变量pt指向Student类对象s时,pt->display()调用后输出则是Student类对象的信息;指针变量pt指向Graduate类对象g时,pt->display()调用后输出则是Graduate类对象的信息。如下图:

        指针变量pt是一个基类指针,可以调用同一类族中不同类的虑函数,这就是多态性,对同一消息,不同对象有不同的响应结果。

        本来,基类指针是用来指向基类对象的,如果用它指向派生类对象,则进行指针类型转换,将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象中的基类部分。

        在基类的display()函数未修改为虚函数前,是无法通过基类指针调用派生类对象中的成员函数的。虚函数突破了这一限制,在派生类的基类部分中,派生类的虚函数取代了基类原来的虚函数,困此在使用基类指针指向派生类对象后,调用虚函数时就调用了派生类的虚函数。

        当把基类的某个成员函数声明为虚函数后,允许在派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。虚函数实现了动态多态性:同一类族中不同类的对象,对同一函数调用作出不同的响应。

        虚函数的使用方法:

  1. 在基类用virtual声明成员函数为虚函数,这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。
  2. 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体(当一个成员函数被声明为虚函数后,基派生类中的同名函数都自动成为虚函数)。
  3. 定义一个指向基类对象的指针变量,并使它指向同一类族中的某一对象。
  4. 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

二、静态关联与动态关联

        在前面的案例中可以看到,同一个display函数在不同的对象中有不同的作用,呈现了多态。对于调用同一类族中的虚函数,应当在调用时用一定的方式告诉编译系统,要调用的是哪个类对象中的函数。例如直接使用对象s.display()或g.display(),这样编译系统在对程序进行编译时,即能确定调用的是哪个类对象中的函数。确定调用的具体对象的过程称为关联(binding)。

        下面通过梳理进一步了解函数重载、虚函数与静态关联和动态关联之间的关系。

2.1 函数重载(Overloading)

  • 函数重载是指在同一作用域内,可以有一组具有相同函数名但参数列表(参数类型、参数个数或参数顺序)不同的函数。
  • 函数重载的解析(即确定调用哪个函数)在编译时完成,因此它是静态关联的。编译器可以根据传递给函数的参数来决定该调用哪个版本的函数。
  • 所以说函数重载属于静态关联(static binding)或早期关联(early binding)是正确的。

2.2 虚函数(Virtual Functions)

  • 虚函数是C++中实现动态多态性的一种方式,通过基类的函数声明前加上virtual关键字,可以使该函数在派生类中被重写(Override)。
  • 当通过基类指针或引用调用虚函数时,实际调用的是指针或引用所指向对象的实际类型(即运行时类型)的虚函数版本。这种在运行时确定调用哪个函数版本的过程称为动态关系(dynamic binding)或滞后关联(late binding)。
  • 通过对象名直接调用虚函数实际上与虚函数的动态关联无关,如果直接通过对象名调用虚函数,那么将直接调用该对象的虚函数版本,而不涉及任何动态关联。

2.3 与关联之间关系

  • 静态关联是在编译时确定函数调用或对象成员访问的过程,通常涉及函数重载、非虚函数调用和直接的对象成员访问。
  • 动态关联是在运行时确定函数调用或对象成员访问的过程,主要涉及通过基类指针或引用调用的虚函数,以及涉及运行时类型信息(RTTI)的其他操作。

2.4 总结

  • 函数重载是静态关联的
  • 通过基类指针或引用调用的虚函数是动态关联的。
  • 直接通过对象名调用虚函数不涉及动态关联,但仍然是虚函数调用(只是不表现出多态性)。

三、何时须声明虚函数

        使用虚函数时需要注意,只能用virtual关键字声明类的成员函数,使它成员虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义,它只能用于类的继承层次结构中。

        什么时候考虑声明虚函数,主要为以下几点:

  1. 首先要看成员函数所在的类是否会作为基类,然后看成员函数在类的继承后有无可能被更改功能。如果希望更改其功能的,一般应该将它声明为虚函数。
  2. 如果成员函数在类被继承后功能不需要修改,或派生类用不到该函数,则不要把它声明为虚函数。
  3. 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用访问的,则应当声明为虚函数。
  4. 有时在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义一个虚函数名,具体功能留给派生类去添加。

        使用虚函数,系统要有一定空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,所以多态性是高效的。

四、虚析构函数

        析构函数的作用是在对象撤销之前做必要的“清理”工作,当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。

4.1 临时对象

        但是如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量,在程序用带指针参数的delete运算符撤销对象时,系统会只执行基类的析构函数,而不执行派生类的析构函数。示例代码如下:

#include <iostream>
using namespace std;
// 声明类Point
class Point{public:Point(float a = 0, float b = 0): x(a), y(b){}// 定义Point析构函数~Point(){cout <<"executing Point destructor" <<endl;}protected:float x, y;
};// 声明Circle类,并公有继承基类Point
class Circle: public Point{private:float radius;public:Circle(float x = 0, float y = 0, float r = 0): Point(x, y), radius(r){}		// 定义Circle的析构函数~Circle(){cout <<"executing Circle destructor" <<endl;}		
};int main(){Point *p = new Circle;delete p;return 0;
}

        运行结果可见,只执行了基类Point中的析构函数,而派生类Circle类中的析构函数未执行。原因是当通过指向基类的指针或引用来删除(或销毁)一个派生类对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。如下图:

4.2 定义虚析构函数

        如上例所示,这种结果可能导致资源泄露或其他未定义行为,因为派生类可能拥有一些需要析构时释放的资源(如动态分配的内存、文件句柄等)。为了解决这个问题,应当将基类中声明一个虚析构函数。代码如下:

#include <iostream>
using namespace std;
// 声明类Point
class Point{public:Point(float a = 0, float b = 0): x(a), y(b){}// 定义Point析构函数virtual ~Point(){cout <<"executing Point destructor" <<endl;}protected:float x, y;
};// 声明Circle类,并公有继承基类Point
class Circle: public Point{private:float radius;public:Circle(float x = 0, float y = 0, float r = 0): Point(x, y), radius(r){}		// 定义Circle的析构函数~Circle(){cout <<"executing Circle destructor" <<endl;}		
};int main(){Point *p = new Circle;delete p;return 0;
}

        运行结果如下图:

        先调用派生类的析构函数,再调用基类的析构函数,比较符合人们正常愿望。这样,所有资源都能得到正确的清理。

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

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

相关文章

遥控挖掘机之ESP8266调试心得(1)

ESP8266调试心得 1. 前言2.遇到的问题2.1 ESP8266模块建立TCP连接时候报错2.2 指令异常问题 3. 更新ESP8266固件3. ESP8266的部分AT指令3. 连接步骤3.1 模块与电脑连接3.2.1 电脑上的设置3.2.2 ESP8266模块作为客户机&#xff08;TCP Cilent&#xff09;的设置步骤 3.2 模块与模…

电脑硬盘故障,这5种情况要了解!

在数字化时代&#xff0c;电脑硬盘作为存储数据的重要设备&#xff0c;其稳定性和安全性直接关系到用户的数据安全和工作效率。然而&#xff0c;硬盘故障却是一个无法完全避免的问题。为什么会出现电脑硬盘故障&#xff1f;出现该问题时应该如何解决&#xff1f;一文带你弄懂答…

k8s部署最新版zookeeper集群(3.9.2),并配置prometheus监控

目录 zookeeper集群部署创建zookeeper文件夹namespace.yamlscripts-configmap.yamlserviceaccount.yamlstatefulset.yamlsvc-headless.yamlsvc.yamlmetrics-svc.yaml执行部署 接入prometheus访问prometheus查看接入情况导入zookeeper监控模版监控展示 zookeeper集群部署 复制粘…

Linux 操作系统TCP、UDP

1、TCP服务器编写流程 头文件&#xff1a; #include <sys/socket.h> 1.1 创建套接字 函数原型&#xff1a; int socket(int domain, int type, int protocol); 参数&#xff1a; domain: 网域 AF_INET &#xff1a; IPv4 AF_INET6 &a…

第十五届蓝桥杯省赛大学B组(c++)

很幸运拿了辽宁赛区的省一,进入6月1号的国赛啦... 这篇文章主要对第十五届省赛大学B组(C)进行一次完整的复盘,这次省赛2道填空题6道编程题: A.握手问题 把握手情景看成矩阵: 粉色部分是7个不能互相捂手的情况 由于每个人只能和其他人捂手, 所以黑色情况是不算的 1和2握手2和…

Vue+OpenLayers7入门到实战:OpenLayers解析通过fetch请求的GeoJson格式数据,并叠加要素文字标注,以行政区划边界为例

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7入门到实战 前言 本章介绍如何使用OpenLayers7在地图上通过fetch请求geojson数据,然后通过OpenLayers解析为Feature要素叠加到图层上,并且通过动态设置标注方式显示要素属性为文字标注。 本章还是以行政区划边界为例,这个…

大模型LLM之SFT微调总结

一. SFT微调是什么 在大模型的加持下现有的语义理解系统的效果有一个质的飞跃&#xff1b;相对于之前的有监督的Pre-Train模型&#xff1b;大模型在某些特定的任务中碾压式的超过传统nlp效果&#xff1b;由于常见的大模型参数量巨大&#xff1b;在实际工作中很难直接对大模型训…

游戏陪玩平台app小程序H5源码交付游戏陪玩接单软件游戏陪玩源码 陪玩小程序陪玩工作室运营模式陪玩管理系统游戏陪玩工作室怎么做

提供陪玩平台源码&#xff0c;陪玩系统源码&#xff0c;陪玩app源码&#xff0c;团队各部门配备齐全&#xff0c;分工明确&#xff0c;及时对接开发进度&#xff0c;保证开发效率 一、陪玩平台源码的功能介绍 1、派单大厅:陪玩系统源码的派单大厅内支持用户通过语音连麦的方式…

Vue.js-----vue组件

能够说出vue生命周期能够掌握axios的使用能够了解$refs, $nextTick作用能够完成购物车案例 Vue 生命周期讲解 1.钩子函数 目标&#xff1a;Vue 框架内置函数&#xff0c;随着组件的生命周期阶段&#xff0c;自动执行 作用: 特定的时间点&#xff0c;执行特定的操作场景: 组…

硬性清空缓存的方法

前端发布代码后&#xff0c;我们是需要刷新页面再验证的。有时候仅仅f5 或者ctrlshiftdelete快捷键仍然有历史缓存&#xff0c;这时可以通过下面的方法硬性清空缓存。 以谷歌浏览器为例&#xff0c;打开f12&#xff0c;右键点击刷新按钮&#xff0c;选择【清空缓存并硬性加载】…

Windows只能安装在GPT磁盘上

转换磁盘分区形式 步骤1. 先按照正常流程使用Windows系统安装光盘或系统U盘引导计算机。 步骤2. 在Windows安装程序中点击“开始安装”&#xff0c;然后按ShiftF10打开命令提示符。 步骤3. 依次输入以下命令&#xff0c;并在每一行命令后按一次Enter键执行。 步骤4. 等待转换…

C++ | Leetcode C++题解之第78题子集

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<int> t;vector<vector<int>> ans;void dfs(int cur, vector<int>& nums) {if (cur nums.size()) {ans.push_back(t);return;}t.push_back(nums[cur]);dfs(cur 1, nums);t.po…

自动驾驶学习2-毫米波雷达

1、简介 1.1 频段 毫米波波长短、频段宽,比较容易实现窄波束,雷达分辨率高,不易受干扰。波长介于1~10mm的电磁波,频率大致范围是30GHz~300GHz 毫米波雷达是测量被测物体相对距离、相对速度、方位的高精度传感器。 车载毫米波雷达主要有24GHz、60GHz、77GHz、79GHz四个频段。 …

深度学习基础之《TensorFlow框架(17)—卷积神经网络》

一、卷积神经网络介绍 1、背景 随着人工智能需求的提升&#xff0c;我们想要做复杂的图像识别&#xff0c;做自然语言处理&#xff0c;做语义分析翻译等等&#xff0c;多层神经网络的简单叠加显然力不从心 2、卷积神经网络与传统多层神经网络对比 &#xff08;1&#xff09;传…

实战 | 18行代码轻松实现人脸实时检测【附完整代码与源码详解】Opencv、人脸检测

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

【图像识别】Swin Transformer

一、引言 论文&#xff1a; Swin Transformer: Hierarchical Vision Transformer using Shifted Windows 作者&#xff1a; Microsoft Research Asia 代码&#xff1a; Swin Transformer 特点&#xff1a; 提出滑动窗口自注意力 (Shifted Window based Self-Attention) 解决Vi…

【3D基础】坐标转换——地理坐标投影到平面

汤国安版GIS原理第二章重点 1.常见投影方式 https://download.csdn.net/blog/column/9283203/83387473 Web Mercator投影&#xff08;Web Mercator Projection&#xff09;&#xff1a; 优点&#xff1a; 在 Web 地图中广泛使用&#xff0c;易于显示并与在线地图服务集成。在…

java.net.SocketInputStream.socketRead0 卡死导致 tomcat 线程池打满的问题

0 TL;DR; 问题与原因&#xff1a;某些特定条件下 java.net.SocketInputStream.socketRead0 方法会卡死&#xff0c;导致运行线程一直被占用导致泄露采用的方案&#xff1a;使用监控线程异步监控卡死事件&#xff0c;如果发生直接关闭网络连接释放链接以及对应的线程 1. 问题 …

Burp Suite 抓包,浏览器提示有软件正在阻止Firefox安全地连接到此网站

问题现象 有软件正在阻止Firefox安全地连接到此网站 解决办法 没有安装证书&#xff0c;在浏览器里面安装bp的证书就可以了 参考&#xff1a;教程合集 《H01-启动和激活Burp.docx》——第5步

WhisperCLI-本地部署语音识别系统;Mis开源LLM推理平台;Dokploy-开源版Vercel;Mem-大规模知识图谱

1. Whisper-cli&#xff1a;可本地部署的开源语音识别系统 近日&#xff0c;Ruff的开发团队发布了一款名为Whisper cpp cli的全新语音识别系统&#xff0c;该系统已在GitHub Repo上开源。这是一款完全自主研发的语音转文字系统&#xff0c;基于Whisper技术构建。Ruff团队一直以…