C++类继承基础2——虚函数和纯虚函数

news2024/12/1 12:09:41

虚函数

如前所述,在C++语言中,当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定。

因为我们直到运行时才能知道到底调用了哪个版本的虚函数,所以所有虚函数都必须有定义。

通常情况下,如果我们不使用某个函数,则无须为该函数提供定义。

但是我们必须为每一个虚函数都提供定义,而不管它是否被用到了,这是因为连编译器也无法确定到底会使用哪个虚函数。

对虚函数的调用可能在运行时才被解析

当某个虚函数通过指针或引用调用时,编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。

被调用的函数是与绑定到指针或引用上的对象的动态类型相匹配的那一个。

#include<iostream>
using namespace std;
class A {
public:
	virtual void a()
	{
		cout << "基类的a函数" << endl;
	}
};

class B :public A {
public:
	virtual void a()
	{
		cout << "派生类的a函数" << endl;
	}

};
void C(A& t)
{
	t.a();
}
int main()
{
	A a;
	B b;
	C(a);//调用A::a()
	C(b);//调用B::a()
}


在第一条调用语句中,参数t绑定到A类型的对象上,因此当C函数调用a函数时,运行的是A::a()

在第二条调用语句中,情况也是类似的

必须要搞清楚的一点是,动态绑定只有当我们通过指针或引用调用虚函数时才会发生

a = b;
a.a();//调用A::a()

当我们通过一个具有普通类型(非引用非指针)的表达式调用虚函数时,在编译时就会将用的版本确定下来。

例如,如果我们使用 a 调用 a(),则应该运行a()的哪个版本是显而易见的。我们可以改变a表示的对象的值(即内容),但是不会改变该对象的类型。因此,在编译时该调用就会被解析成A的a().

关键概念:C++的多态性
OOP 的核心思想是多态性(polymorphism)。多态性这个词源自希腊语,其含义是甜形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数直正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对.如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。
另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虎看数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时编定到该对象所属类中的函数版本上。

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有Note在这种情况下对象的动态类型才有可能与静态类型不同。

派生类中的虚函数

当我们在派生类中覆盖了某个虚函数时,可以再一次使用virtual关键字指出该函数的性质。

然而这么做并非必须,因为一旦某个函数被声明成虚函数,则在所有派生类中它都是虚函数.

一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。

同样,派生类中虚函数的返回类型也必须与基类函数匹配。

该规则存在一个例外,当类的虚函数返回类型是类本身的指针或引用时,上述规则无效。

也就是说,如果D由B派生得到,则基类的虚函数可以返回B*,而派生类的对应函数可以返回D*,只不过这样的返回类型要求从D到B的类型转换是可访问的。

基类中的虚函数在派生类隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类的形参必须和派生类中的形参严格匹配

final和 override 说明符

派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的行为,

编译器将认为新定义的这个的影写基类中原有的函数是相互独立的。

这时,派生类的函数并没有覆盖掉基类中的版本。

class A {
public:
	virtual void a()
	{
		cout << "基类的a函数" << endl;
	}
};

class B :public A {
public:
	 void a(int a)//没有覆盖虚函数
	{
			}

};

就实际的编程习惯而言,这种声明往往意味着发生了错误,因为我们可能原本希望派生类能覆盖掉基类中的虚函数,但是一不小心把形参列表弄错了。

要想调试并发现这样的错误显然非常困难。

在C++11 新标准中我们可以使用override关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误,后者在编程实践中显得更加重要。

如果我们使用 override 标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错:

struct B {

virtual void fl(int)const;
virtual void f2();
voia f3();
};
struct D : B {

void fl(int)const override; //正确:f1与基类中的fl匹配
void f2(int) override; //错误:B没有形如f2(int)的函数
void f3() override; //错误:f3不是虚函数
void f4() override; // 错误:B没有名为f4的函数
};

在D1中,f1的override说明符是正确的,因为基类和派生类中的f1都是const成员,并且它们都接受一个int返回 void,所以D1中的f1正确地覆盖了它从B中继承而来的虚函数。

D1中f2的声明与B中f2的声明不匹配,显然B中定义的f2不接受任何参数而D1的f2接受一个int。因为这两个声明不匹配,所以D1的f2不能覆盖B的f2,它是一个新函数,仅仅是名字恰好与原来的函数一样而已。

因为我们使用 override 所表达的意思是我们希望能覆盖基类中的虚函数而实际上并未做到,所以编译器会报错。

因为只有虚函数才能被覆盖,所以编译器会拒绝D1的f3。该函数不是B中的虚函数,因此它不能被覆盖。类似的,f4的声明也会发生错误,因为B中根本就没有名为f4的函数。

我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误:

struct D2 :B {
//从B继承£2()和f3(),覆盖f1(int)
void f1(int) const final; //不允许后续的其他类覆盖f1(int)
};
struct D3 : D2 {
void f2(); // 正确:覆盖从间接基类B继承而来的£2

void fl(int) const;// 错误:D2 已经将 f2 声明成 final
};

final和override说明符出现在形参列表(包括任何const和引用说明符)以及尾置返回类型之后。

虚函数与默认实参

和其他函数一样,虚函数也可以拥有默认实参。

如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。

换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参即使实际运行的是派生类中的函数版本也是如此。

此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。

如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。

回避虚函数的机制

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版

本。使用作用域运算符可以实现这一目的,例如下面的代码:

// 强行调用基类中定义的函数版本而不管 baseP的动态类型到底是什么
double undiscounted =basep->Quote::net_price (42);


该代码强行调用Quote的net_price函数,而不管basep实际指向的对象类型到底是什么。该调用将在编译时完成解析。

通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。

什么时候我们需要回避虚函数的默认机制呢?

通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。

在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。

如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用城运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

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

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

相关文章

C++:继承的介绍和深度解析

一、继承的概念和定义 1.什么是继承&#xff1f; 继承&#xff0c;顾名思义&#xff1a;就和现实生活中&#xff0c;孩子继承父母的东西有点类似。比如&#xff0c;你父亲的财产&#xff0c;你可以继承下来&#xff0c;你就可以使用父亲的钱。 官方一点的介绍&#xff1a; 继承…

代码随想录阅读笔记-二叉树【对称二叉树】

题目 给定一个二叉树&#xff0c;检查它是否是镜像对称的。 思路 首先想清楚&#xff0c;判断对称二叉树要比较的是哪两个节点&#xff0c;要比较的可不是左右节点&#xff01; 对于二叉树是否对称&#xff0c;要比较的是根节点的左子树与右子树是不是相互翻转的&#xff0…

2024 ccfcsp认证打卡 2021 12 01 序列查询

2021 12-1 序列查询 题解1题解2区别第一种算法&#xff1a;第二种算法&#xff1a; 题解1 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);// 输入n表示商品数目&#xff0c;N表示总数int n sc.n…

使用Vite安装TailwindCSS

一、认识TailwindCSS Tailwind CSS 是一个基于原子类的 CSS 框架&#xff0c;它提供了一种不同于传统 CSS 框架的方式来构建用户界面。下面是关于 Tailwind CSS 的优缺点以及它适合应用的情况&#xff1a; 优点&#xff1a; 灵活性&#xff1a; Tailwind CSS 提供了大量的原…

Portal Particle

Unity3D Portal Particle 2.2传送门粒子效果 链接&#xff1a;https://pan.baidu.com/s/1TCMXIif5d288lXHgixnDPw?pwd1234 下载&#xff1a;资源下载链接 效果图&#xff1a;

Java虚拟机(JVM)知识点总结

一. Java内存区域 1. JVM的内存区域划分&#xff0c;以及各部分的作用 可分为运行时数据区域和本地内存&#xff0c;按照线程私有和线程共享分类&#xff1a; 线程私有&#xff1a;程序计数器、虚拟机栈、本地方法栈。 线程共享&#xff1a;堆、方法区、直接内存。 JDK1.7…

V R虚拟现实元宇宙的前景|虚拟现实体验店加 盟合作|V R设备在线购买

VR&#xff08;虚拟现实&#xff09;技术作为一种新兴的技术&#xff0c;正在逐渐改变人们的生活和工作方式。随着技术的不断进步&#xff0c;人们对于元宇宙的概念也越来越感兴趣。元宇宙是一个虚拟世界&#xff0c;通过VR技术可以实现人们在其中进行各种活动和交互。 元宇宙的…

(C++17) std算法之执行策略 execution

文章目录 前言Code测试Code运行效果 msvc源码描述源码std::sequenced_policy seqstd::parallel_policy parstd::parallel_unsequenced_policy par_unseqstd::unsequenced_policy unseq END 前言 ref:算法库-执行策略 - cppreference.com 利用多核cpu加速算法在目前看来已经不是…

Springboot+MybatisPlus+EasyExcel实现文件导入数据

记录一下写Excel文件导入数据所经历的问题。 springboot提供的文件处理MultipartFile有关方法&#xff0c;我没有具体看文档&#xff0c;但目测比较复杂&#xff0c; 遂了解学习了一下别的文件上传方法&#xff0c;本文第1节记录的是springboot原始的导入文件方法写法&#xf…

论文阅读-《Lite Pose: Efficient Architecture Design for 2D Human Pose Estimation》

摘要 这篇论文主要研究了2D人体姿态估计的高效架构设计。姿态估计在以人为中心的视觉应用中发挥着关键作用&#xff0c;但由于基于HRNet的先进姿态估计模型计算成本高昂&#xff08;每帧超过150 GMACs&#xff09;&#xff0c;难以在资源受限的边缘设备上部署。因此&#xff0…

vue的创建、启动以及目录结构详解

vue的创建、启动以及目录结构详解目录 一. vue项目的创建 二. vue的目录结构 三. src的目录结构 四. vue项目的启动 4.1 方法1 4.2 方法2 一. vue项目的创建 创建一个工程化的Vue项目&#xff0c;执行命令&#xff1a;npm init vuelatest 注意&#xff1a;如果你在这个目…

【Python基础教程】3 . 算法的时间复杂度

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;python基础教程 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、…

[每周一更]-第91期:认识AMD的CPU

这一章来认识下AMD,说起AMD&#xff0c;从我上大学那时候&#xff0c;就选购的AMD 速龙系列&#xff0c;生活拮据的情况下&#xff0c;拉着室友去郑州电子城&#xff0c;在跟奸商老板的拉扯下&#xff0c;斥资2000购入一个无显卡 的台式机&#xff08;实在是资金有限&#xff0…

【Spring源码】WebSocket做推送动作的底层实例

一、前瞻 Ok&#xff0c;开始我们今天的对Spring的【模块阅读】。 那就挑Web里的WebSocket模块&#xff0c;先思考下本次阅读的阅读线索&#xff1a; WebSocket在Spring里起到什么作用这个模块采用了什么设计模式我们都知道WebSocket可以主动推送消息给用户&#xff0c;那做推…

小程序中使用less

在vscode中安装插件 找到左下角齿轮的设置&#xff0c;点击右边图标&#xff0c;进入“settings.json” 加上以下代码配置 "less.compile":{"outExt": ".wxss"}

在.Net6中用gdal实现第一个功能

目录 一、创建.NET6的控制台应用程序 二、加载Gdal插件 三、编写程序 一、创建.NET6的控制台应用程序 二、加载Gdal插件 Gdal的资源可以经过NuGet包引入。右键单击项目名称&#xff0c;然后选择 "Manage NuGet Packages"&#xff08;管理 NuGet 包&#xff09;。N…

揭秘情绪识别:如何让AI读懂你的心声?

最近我在研究大语言模型&#xff0c;想用它来给样本打分。 起初&#xff0c;我尝试让模型用1到5分来评分&#xff0c;但它总是极端地给出最低分或最高分&#xff0c;评分缺乏中间地带。 于是我换了个方法&#xff0c;不再用数字&#xff0c;而是用描述性的词语&#xff0c;比…

《让你的时间多一倍》逃离时间陷阱,你没有自己想的那么懒 - 三余书屋 3ysw.net

让你的时间多一倍 今天我们来阅读法比安奥利卡尔的作品《让你的时间多一倍》。或许你会心生疑虑&#xff0c;这本书是否又是一本沉闷的时间管理指南&#xff1f;但我要告诉你的是&#xff0c;尽管时间管理这个话题已经为大众所熟知&#xff0c;这本书却为我们揭示了一个全新的…

php将网页用wkhtmltoimage内容生成为图片

php架构ThinkPHP6 1. 安装 knp-snappy架构 composer require knplabs/knp-snappy use Knp\Snappy\Image; use Illuminate\Support\Facades\Storage;// 生成图片 /user/local/bin/wkhtmltoimage为你的wkhtmltoimage的位置。 $snappy new Image(/usr/local/bin/wkhtmltoimage…

EXCEL VBA根据表数据写入数据库中

EXCEL VBA根据表数据写入数据库中 Option Explicithttps://club.excelhome.net/thread-1687531-1-1.htmlSub UpdateAccess()Const adStateOpen 1Dim vData, i As Variant, j As LongDim AccessTable As String, ExcelTable As String, ExcelFile As String, AccessFile As Str…