【第五节】C++的多态性与虚函数

目录

前言

一、子类型

二、静态联编和动态联编

三、虚函数

四、纯虚函数和抽象类

五、虚析构函数

六、重载,重定义与重写的异同

 


前言

        面向对象程序设计语言的三大核心特性是封装性、继承性和多态性。封装性奠定了基础,继承性是实现代码重用和扩展的关键,而多态性则是功能的扩充。多态性体现在对不同类的对象发送相同的消息时,会产生不同的行为。这里所说的消息主要是指对类成员函数的调用,而不同的行为则对应着不同的实现方式。在C++中,实现多态性的方法包括:

  • 函数重载

  • 运算符重载

  • 模板

  • 虚函数

        函数重载是多态性的一种基本形式,它允许在同一作用域内,相同的函数名对应不同的实现。函数重载的实现条件是函数参数的类型或个数必须有所区别。

        除了函数重载这种简单的多态形式,C++还提供了更为灵活的特性——虚函数。虚函数使得函数调用与函数体的绑定可以在运行时动态确定,这对于实现同一接口、多种实现的场景尤为重要。在深入探讨虚函数的概念之前,我们需要先了解子类型、静态联编和动态联编等相关概念。

一、子类型

        在继承的框架下,如果类B以公有继承的方式从类A派生而来,那么类B不仅继承了类A的行为,还可能拥有自身独特的行为。在这种情况下,我们称类B为类A的一个子类型。具体来说,如果存在一个类型S,它至少提供了类型T的行为,那么我们称类型S是类型T的子类型。

        当类B是类A的子类型时,类A对象能够调用的函数,类B的对象同样能够调用。这种情况下,我们称类B与类A兼容,或者说类B适应于类A。子类型的一个重要作用是实现类型兼容性,即在公有继承的模式下,派生类的对象、指向对象的指针以及对象的引用,都能够无缝地适应于基类的对象、指向对象的指针和对象引用所适用的场合。

        需要注意的是,子类型关系是单向且不可逆的。如果已知类B是类A的子类型,那么认为类A也是类B的子类型是不正确的。子类型的概念强调的是派生类对基类的兼容性和扩展性,而不是基类对派生类的依赖。

二、静态联编和动态联编

        联编是程序中各个部分相互关联的过程。根据联编发生的时机,它可以分为静态联编和动态联编两种类型。静态联编,又称为早期联编,发生在程序的编译和链接阶段。在这种联编方式中,函数调用与执行该函数的代码之间的对应关系在程序运行之前就已经确定,这意味着所有关联工作都在程序执行前完成。

示例代码:

class CBase {
public:void fun() {cout << "CBase:fun" << endl;}
};class CMyClass :public CBase {
public:void fun() { cout << "CMyClass:fun" << endl; }
};int main() {CBase* p;CBase objA;CMyClass objB;p = &objA;p->fun();p = &objB;p->fun();return 0;
}

        在静态联编的情况下,如果存在一个指向基类的指针p,那么在程序运行之前,p->fun()就已经被确定为调用基类的成员函数fun()。因此,无论指针p指向的是基类对象还是派生类对象,p->fun()都将调用基类的成员函数,并且结果保持一致。这是静态联编的特性。

        相比之下,动态联编,又称为晚期联编,是在程序运行时进行的联编过程。动态联编要求在运行时确定函数调用与执行该函数代码之间的对应关系。以之前的例子为例,如果采用动态联编,那么随着指针pobjA指向的对象不同,pobjA->fun_a()将能够调用不同类中fun_a()的不同版本。这意味着,通过一个统一的接口pobjA->fun_a(),可以访问多个不同的实现版本,即函数调用取决于运行时pobjA所指向的对象,从而展现出多态性。使用虚函数可以实现动态联编,允许在不同的联编情况下选择不同的实现,这正是多态性的体现。

        继承是实现动态联编的基础,而虚函数则是动态联编的关键所在。通过虚函数,可以在运行时根据对象的实际类型来调用相应的函数版本,从而实现多态行为。

三、虚函数

虚函数的概念:在基类中冠以关键字 virtual 的成员函数
虚函数的定义:
virtual<类型说明符>函数名>(<参数表>)
{
//<函数体>
}

virtual void fun a()
{
//<函数体>
}

虚函数的定义与特性如下:

  1. 若在基类中将某一成员函数声明为虚函数,则该函数在所有派生类中均保持其虚函数的属性,即使派生类中未显式使用virtual关键字。

  2. 动态绑定(或动态联编)仅在通过基类指针或引用调用虚函数时发生,这是实现多态性的关键机制。

  3. 虚函数不能被声明为静态函数,也不能是友元函数,因为这些类型的函数不支持动态绑定。

  4. 在基类中声明为虚函数的成员函数,在派生类中即便没有使用virtual关键字,仍然保持其虚函数的特性。

  5. 当一个成员函数在基类中被声明为虚函数时,它允许在派生类中拥有不同的实现版本,这为多态性的实现提供了可能。

  6. 由于虚函数的存在,编译器会在运行时进行动态联编,确保调用虚函数的对象在运行时根据实际对象类型来确定,从而实现动态联编的多态性。

说了半天虚函数,它到底有什么特性??特性如下:

当一个父类指针指向子类对象的时候,调用一个虚函数,将调用子类的虚函数。

示例代码:

#include <iostream>
using namespace std;class Base {
public:virtual void Fun1() {cout << "Base::Funl ..."<<endl ;}virtual void Fun2() {cout << "Base::Fun2 ..." << endl;}void Fun3() {cout << "Base::Fun3 ..." << endl;}
};class Derived : public Base {
public:/*virtual*/void Fun1()//不加virtual Fun1 也是虚函数。{cout << "Derived::Fun1 ..."<< endl; }/*virtual*/void Fun2(){cout << "Derived::Fun2 ..." << endl;}void Fun3(){cout << "Derived::Fun3 ..." << endl;}};
int main() {Base* p;Derived d;p = &d;p->Fun1(); //Fun1是虚函数,基类之指针指向派生类对象,调用的是派生类对象的虚函数p->Fun2();p->Fun3(); //Fun3非虚函数,根据p指针实际类型来调用相应类的成员函数return 0;
}

运行结果:

        这是一个很厉害的特性,我们知道调用什么函数,一般是和类型绑定的,类A的指针调用fun这个函数,本身调用的应该是类A的函数。但是有了虚函数之后,看这个指针指向哪一个子类对象了。
        这给我们提供了很多想象力,比如一个函数的参数是父类指针,我们往里面传递不同的子类对象,就可以在函数中调用到不同的虚函数。
        再比如,我们在一个数组中存储不同的子类对象,统一的去调用虚函数,大家的行为都是不相同的。

四、纯虚函数和抽象类

        虚函数机制赋予了基类指针指向派生类对象的能力,并确保调用的是派生类中相应的虚函数,这一特性使得我们能够以统一的方式处理不同派生类的对象。这种动态绑定确保了函数入口在运行时才得以确定。然而,当面临基类接口无法实现的情况时,我们该如何应对?以形状类为例,它定义了一个求面积的函数,而圆形和矩形作为其派生类,各自拥有计算面积的方法。但形状本身作为一个抽象概念,并不具备计算面积的具体方法。在这种情况下,纯虚函数便派上了用场。包含纯虚函数的类被称为抽象类,它们不能被实例化。纯虚函数是一种特殊的虚函数,它没有具体的实现,仅作为接口存在,强制派生类提供必要的实现细节。

其定义格式如下:

class <类名>
{
    virtual <函数类型> <函数名>(<参数表>)=0;
    //...
}

class CClassA
{
    virtual <函数类型> <函数名>(<参数表>)=0;
    //...
}

        在众多场景中,基类可能无法为虚函数提供实质性的实现,此时将其声明为纯虚函数,将其实现的责任转交给派生类,这正是纯虚函数的核心作用。当一个类中包含了纯虚函数,它便成为了抽象类。根据C++的规范,抽象类无法直接创建对象。

        由于纯虚函数缺乏具体的实现,包含此类函数的类自然无法直接实例化,这一点显而易见——因为无法调用未实现的纯虚函数。因此,这类类被形象地称为抽象类。抽象类若要摆脱其抽象的本质,唯有依赖派生类来充实这些虚函数的具体实现。

示例代码:

#include <iostream>
using namespace std;class Shape {
public:virtual void Draw() = 0;virtual ~Shape() {}
};class Circle :public Shape{
public:void Draw() {cout << "Circle::Draw()..." << endl;}~Circle() {cout << "~Circle ..." << endl;}};class Square : public Shape {
public:void Draw() {cout << "Square::Draw()" << endl;}~Square() {cout << "~Square ..." << endl;}
};
int main() {//Shape obj; //错误的,抽象类不能定义对象Shape* pobj = NULL;Circle objcirele;pobj = &objcirele;pobj->Draw();return 0;
}

        抽象类仅能作为基类被继承,而不能直接声明抽象类的实例。在类的构造与析构过程中,构造函数不可设为虚函数,而析构函数则允许为虚函数。

        抽象类本身不具备直接创建对象实例的能力,但可以声明抽象类的指针或引用。通过指向抽象类的指针,我们能够实现运行时的多态性。派生类有义务实现基类中的纯虚函数,若未能履行这一职责,该派生类仍将被视为抽象类。

五、虚析构函数

        构造函数不可声明为虚函数,而析构函数则具备这一特性,通过在析构函数前添加关键字virtual来实现。一旦基类的析构函数被声明为虚函数,其派生类的析构函数默认也成为虚析构函数,此时可省略virtual关键字。

        将析构函数声明为虚函数的原因在于,当基类指针指向派生类对象时,这是多态性的常见应用场景。在释放内存时,若通过delete操作符删除基类指针,通常只会触发基类的析构函数,而派生类的析构函数则不会被调用,这可能导致内存泄漏。

        然而,若基类的析构函数是虚函数,且派生类提供了自定义的析构函数实现,那么在delete基类指针时,将同时调用派生类的析构函数。在派生类执行析构过程时,会自动调用基类的析构函数,确保所有相关资源得到妥善清理。

示例代码:

#include <iostream>
using namespace std;class CClassA{
public :CClassA(){cout << "CClassA" << endl; }virtual ~CClassA() {cout << "~CClassA" << endl; }
};class CClassB : public CClassA {
public:CClassB() { cout << "CClassB" << endl; }virtual ~CClassB() { cout << "~CClassB" << endl; }
};int main() {CClassA* pobjA = new CClassB;delete pobjA;return 0;
}

六、重载,重定义与重写的异同

        在面向对象编程中,"重载"(overload)、"重写"(override)和"重定义"(redefine)是三个重要的概念,它们在处理成员函数时有着不同的应用和特征。

重载(Overload)

  • 发生在同一个类中。

  • 函数名称相同。

  • 参数列表必须不同。

  • 是否使用virtual关键字是可选的。

重写(Override)

  • 发生在派生类与基类之间。

  • 函数名称相同。

  • 参数列表相同。

  • 基类中的函数必须声明为virtual

重定义(Redefine)

  • 发生在派生类与基类之间。

  • 当函数名和参数都相同时,基类函数不需要virtual关键字。

  • 当函数名相同但参数不同时,是否使用virtual关键字是可选的。

这些概念的理解和正确应用对于掌握面向对象编程至关重要,它们帮助开发者以更加灵活和高效的方式设计和实现类和对象。

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

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

相关文章

Linux内网中安装jdk1.8详细教程

本章教程,主要介绍如何在内网环境中配置JDK1.8环境变量 一、下载Linux版压缩包 下载地址:https://www.oracle.com/java/technologies/downloads/#java8 下载完成之后,通过XFTP等工具,将安装包上传到内网服务器 二、安装配置步骤 1、解压压缩包 tar -zxvf /usr/local/jdk-…

linux--自动备份文件

问题&#xff1a; 1&#xff0c;rm删除无法找回&#xff1b; 2&#xff0c;使用git的时候会出现各种可能导致文件丢失&#xff0c;无法找回的情况。 3&#xff0c;......。 设置自动备份文件和目录

使用Python, 用shp文件边界裁剪tif文件

在Python中, 用shp文件边界裁剪tif文件 from osgeo import gdal import osgdal.PushErrorHandler("CPLQuietErrorHandler")def subset_by_shp(shape_fn, raster_fn, raster_out):"""根据 shapefile 对栅格文件进行裁剪并输出结果:param shape_fn: sh…

根据PDF模版填充数据并生成新的PDF

准备模版 使用 福昕高级PDF编辑器 &#xff08;本人用的这个&#xff0c;其他的也行&#xff0c;能作模版就行&#xff09;打开PDF文件点击 表单 选项&#xff0c;点击 文本域在需要填充数据的位置设计文本域设置 名称、提示名称相当于 属性名&#xff0c;提示就是提示&#x…

基于SSM的“基于Apriori算法的网络书城”的设计与实现(源码+数据库+文档)

基于SSM的“基于Apriori算法的网络书城”的设计与实现&#xff08;源码数据库文档) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SSM 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 网站功能展示图 首页 商品分类 热销 新品 我的订单 个…

二位偏序,P3660 [USACO17FEB] Why Did the Cow Cross the Road III G

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 P3660 [USACO17FEB] Why Did the Cow Cross the Road III G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 二、解题报告 1、思路分析 二维偏序问题 我们将坐标按照第一维排序 然后树状数组维护区间内的…

【深度学习】【STWave】时空图预测,车流量预测,Efficient Spectral Graph Attention Network

Spatio-Temporal meets Wavelet: Disentangled Traffic Flow Forecasting via Efficient Spectral Graph Attention Network 代码&#xff1a;https://github.com/LMissher/STWave 论文&#xff1a;https://arxiv.org/abs/2112.02740 帮助&#xff1a; https://docs.qq.com/s…

C++STL---vector模拟实现

通过上篇文章&#xff0c;我们知道vector的接口实际上和string是差不多的&#xff0c;但是他俩的内部结构却大不一样&#xff0c;vector内有三个成员变量&#xff1a;_start、_finish、_endofstorage: _start指向容器的头元素&#xff0c;_finish指向有效元素末尾的元素&#x…

Vue2 + Element UI 封装 Table 递归多层级列表头动态

1、在 components 中创建 HeaderTable 文件夹&#xff0c;在创建 ColumnItem.vue 和 index.vue。 如下&#xff1a; 2、index.vue 代码内容&#xff0c;如下&#xff1a; <template><div><el-table:data"dataTableData"style"width: 100%"…

OSM历史10年(2014-2024)全国数据下载(路网、建筑物、POI、水系、地表覆盖利用······)

点击下方全系列课程学习 点击学习—>ArcGIS全系列实战视频教程——9个单一课程组合系列直播回放 零、前沿 这次向大家介绍一下OSM&#xff08;OpenStreetMap&#xff09;十年历史数据&#xff08;2014—2014&#xff09;的下载方法。当然我们也下载好分享给大家&#xff…

JAVA web期末复习总结

C/S结构与B/S结构区别&#xff1a; 在C/S结构中&#xff0c;客户端通常是一个独立的应用程序&#xff0c;需要在用户的计算机上安装和运行。而在BS结构中&#xff0c;客户端是一个Web浏览器&#xff0c;用户只需要通过浏览器打开网页&#xff0c;不需要安装额外的应用程序。 C…

python正则表达式中的分组功能

在Python的re模块中&#xff0c;group()方法是用于从一个匹配的对象&#xff08;例如&#xff0c;re.match或re.search返回的对象&#xff09;中提取匹配的字符串。 当你使用正则表达式进行匹配时&#xff0c;匹配对象会包含原始字符串中与模式匹配的部分。group()方法可以用来…

Reactive 踩坑

vue 响应式踩坑 let questionInfo reactive([ , ]) api.getQuestions( id ).then(function (response){// 这里用法有问题questionInfo response.data.data.questions;concole.log(questionInfo) })响应式数据本身是个函数&#xff0c;&#xff08;不然咋帮你动态变化页面…

k8s_设置dns

配置k8s dns 在 Kubernetes 集群中&#xff0c;CoreDNS 是默认的 DNS 服务器&#xff0c;它负责处理集群内所有的 DNS 请求。 kubectl edit cm coredns -n kube-system (此命令修改coredns 配置) kubectl describe cm coredns -n kube-system&#xff08;此命令查看coredns 配…

程序员上岸指南

如果你还在996&#xff0c;大小周&#xff0c;感觉身体被掏空&#xff0c;那么你可以看看下面这篇文章&#xff0c;我特意搜集了一些苦逼程序员的上岸教程。 人生真的就是做几道选择题&#xff0c;选错了&#xff0c;忙也是瞎忙。选对了&#xff0c;躺着都能赢。总的来说&#…

良心推荐:什么软件能够监控公司电脑,就这5款软件能监控公司电脑

用软件监控员工电脑在大多数国家是非法的&#xff0c;不过在中国&#xff0c;企业在办公场所安装监控设备以监控员工工作&#xff0c;在遵循一定限制和条件的前提下&#xff0c;是合法的。以下是一些符合这些条件&#xff0c;并且广泛被企业采用的员工电脑监控软件的良心推荐&a…

Linux 命令操作技巧

Linux命令行界面提供了丰富的快捷键来提高操作效率&#xff0c;以下是一些常用的Linux终端快捷键&#xff0c;主要基于Bash shell&#xff1a; Tab - 自动补全&#xff1a;输入命令、文件名、目录名或命令选项的开头部分&#xff0c;然后按Tab键&#xff0c;系统会自动补全剩余…

C++:儿童节快乐呀!

六一快乐&#xff01;~ 今天我来宣布一个 ——————Cookie Maker工作室成立了&#xff01;—————— 目前参与人数&#xff1a;7 人名列单&#xff1a; 真实姓名 联系方式以及用户名 ZINCFFO CSDN&#xff08;ZINCFFO&#xff09; &…

使用 Scapy 库编写 IP 地址欺骗攻击脚本

一、介绍 1.1 概述 IP地址欺骗&#xff08;IP Spoofing&#xff09;是一种网络攻击技术&#xff0c;攻击者伪造其数据包的源IP地址&#xff0c;使其看起来像是从其他合法地址发送的。这种技术常用于各种攻击中&#xff0c;例如DDoS攻击、Man-in-the-Middle&#xff08;MITM&a…

buidldroot musl uclib库 编译

buildroot 修改 编译工具链 原本编译器相关信息&#xff1a; Incorrect selection of the C library buidroot编译 注意相关选项&#xff0c;后续使用CUSTOM TOOLCHAIN 时对应 UCLIB 能将生成IMAGE 从2.9K变为2.3K MUSL 能将生成IMAGE 从2.9K变为2.7K 变大了 arm-linux-…