C++:多态中的虚/纯虚函数,抽象类以及虚函数表

我们在平时,旅游或者是坐高铁或火车的时候。对学生票,军人票,普通票这些概念多少都有些许耳闻。而我们上篇文章也介绍过了继承与多继承。如果这些票我们都分别的去写一个类,当然很冗余,这里我们便可以去使用继承,我们假设我们的票价是由一个票价函数控制的,如果子类与父类中有着同名的票价函数,我们之前也介绍过他会隐藏,那我们要如何去实现使用不同的子类达到不同的效果呢--答案就是多态。

一,多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点介绍运行时多态,编译时多态(静态多态)和运行时多态(动态多态)编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。 

运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。 其实就是我们上面的买票行为,不同的人买票对应的价格也不同。

 

二,构成多态的前提与两个重要条件

构成多态的前提是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。 而实现多态则必须具备以下两个重要条件:

  1. 必须指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数。

需要注意的是,引用的指针必须为父类指针。而且被调用的虚函数必须要在父类中也为虚函数,这样才能被子类重写覆盖:
 

class parent
{
public:parent(int a = 1):_a(a){}void print(){cout << _a << endl;}
private:int _a;
};class child
{
public:child(int b = 2):_b(b){}                  //这是一个虚函数,但父类中对应完全相同函数没有virtual前缀,virtual void print()//所以没有构成重写,也就不会形成多态{cout << _b << endl;}private:int _b;
};

而我们如果想要实现多态,一个是在父类的完全相同函数(返回值,函数名,参数完全相同) 前加上virtual前缀,另一点则需要用父类指针去调用子类对象的对应虚函数,此时才能形成多态:

int main()
{child c;parent& p1 = c;p1.print();//构成多态parent* p2 = &c;p2->print();//构成多态parent p3 = c;p3.print();//不构成多态return 0;
}

从运行结果我们可以清晰的看到必须重写和使用父类指针两个条件同时满足才能实现多态。 

三,虚函数与虚函数的重写与覆盖 

3.1虚函数的定义方式

class parent
{
public:virtual void print(){cout << _a << endl;}
private:int _a;
};

 类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修
饰。(比如类中的静态成员函数, 所有子类公用与父类相同的静态成员,也正是因为静态成员函数无法变为虚函数,因此静态成员函数无法形成多态)。

3.2虚函数的重写/覆盖 

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

但是我们平时可能会看到如下的情况:

class parent
{
public:parent(int a = 1):_a(a){}virtual void print(){cout << _a << endl;}
private:int _a;
};class child : public parent
{
public:child(int a = 1,int b = 2):_b(b),parent(a){}void print(){cout << _b << endl;}private:int _b;
};

此时子类的完全相同函数虽然没有加上virtual前缀,但实际上也构成了重写,不过这种写法并不规范。也正是这种原因,它经常会被作为面试/笔试的考题出现。 

3.3虚函数中的协变 

 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解一下即可。


private:int _b;
};class A {};
class B : public A {};
class Person {
public:virtual A* BuyTicket(){                              //cout << "买票-全价" << endl;//return nullptr;            //这就是一种协变,返回值不同,}                              //但返回的对象互为父子类,所以
};                                 //也会构成重写。
class Student : public Person {    //
public:virtual B* BuyTicket(){cout << "买票-打折" << endl;return nullptr;}
};

注意,返回的不一定必须是当前类的父子类指针/引用,也可以是其他父子类指针/引用。只要返回的对像构成父子类关系以及同为指针/引用即可。

3.4override关键字与final关键字 

 override关键字可以帮助我们检查虚函数是否构成覆写,而final关键字则可以使虚函数无法被覆写:

// error C3668: “Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类方法
class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
// error C3248: “Car::Drive”: 声明为“final”的函数无法被“Benz::Drive”重写
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};

3.5总结:重载/重写/隐藏的对比

 

四,纯虚函数与抽象类 

在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。 

class parent//抽象类
{
public:parent(int a = 1):_a(a){}virtual void print() = 0;//纯虚函数
private:int _a;
};class child : public parent
{
public:child(int a = 1,int b = 2):_b(b),parent(a){}void print(){cout << _b << endl;}private:int _b;
};

五,多态的原理 

5.1虚函数表

当我们创建了一个类时,它所占用的实际大小是虚表与成员变量所占空间之和:

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};

比如上面这个类,它的一个实例化对像实际大小为12Byte。其中八个字节存放两个成员变量,另四个字节用来存放虚表(__vfptr)放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

 

5.1.1虚函数表的相关概念与知识 

  1. 基类对象的虚函数表中存放基类所有虚函数的地址。
  2. 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基
  3. 类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。
  4. 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
  5. 派生类的虚函数表中包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址三个部分。
  6. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)
  7. 虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
  8. 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,vs下是存在代码段(常量区) 

5.2多态实现原理 

我们拿上面的parent与child类来说明:

class parent
{
public:parent(int a = 1):_a(a){}void print(){cout << _a << endl;}
private:int _a;
};class child
{
public:child(int b = 2):_b(b){}                  //这是一个虚函数,但父类中对应完全相同函数没有virtual前缀,virtual void print()//所以没有构成重写,也就不会形成多态{cout << _b << endl;}private:int _b;
};

 

通过上图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

所以我们在使用父类指针去调用子类对象中构成重写的虚函数时,实际上它并不是到父类中去调用父类完全相同虚函数再对其重写,而是通过虚表在运行时确定要调用的虚函数,所以最终调用的虚函数是由调用指针指向的对像决定的而不是由指针的类型决定。

5.2.1动态绑定与静态绑定 

对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数
的地址
,也就做动态绑定。 

5.3虚函数表的一些其他注意点 

如果我们有以下一段代码:

 

class parent
{
public:parent(int a = 1):_a(a){}virtual void print(){cout << _a << endl;}
private:int _a;
};class child
{
public:child(int b = 2):_b(b){}                  virtual void print(){cout << _b << endl;}virtual void print1(){}private:int _b;
};

print1虚函数是否会存在与虚函数表中?答案是存在的,我们在vs下会看到以下情景:

 

虽然print1没有构成重写,但它依然存放与虚函数表中。但有时我们会在vs下遇到看不到print1函数在虚表中的场景,这时我们可以使用内存窗口查看,便可以看到以下情况:

即可证明print1函数存放于虚表中。

 

 

 

 

 

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

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

相关文章

【易售校园二手平台】开源说明(包含项目介绍、界面展示与系列文章集合)

文章目录 仓库项目介绍技术架构界面登录界面首页闲置商品发布商品详情收藏页面消息页面私聊我的查看我发布的商品 可优化点开发讲解文章集合 仓库 &#x1f3e0;️ 项目仓库&#xff1a;易售校园二手平台gitee仓库 &#x1f30d;️ 在线体验&#xff1a;易售校园二手平台&…

python怎么将字符串转换为数字

python如何将列表中的字符串转为数字&#xff1f;具体方法如下&#xff1a; 有一个数字字符的列表&#xff1a; numbers [1, 5, 10, 8] 想要把每个元素转换为数字&#xff1a; numbers [1, 5, 10, 8] 用一个循环来解决&#xff1a; new_numbers []; for n in numbers:new_n…

APP 后台广告位配置的关键要素与策略

在当今数字化营销的浪潮中&#xff0c;APP 作为重要的信息传播渠道&#xff0c;其后台广告位的配置显得尤为关键。这不仅影响着广告的展示效果&#xff0c;还直接关系到用户体验和平台收益。 首先&#xff0c;了解目标受众是配置广告位的基础。通过对 APP 用户的行为数据进行分…

创建MoveIt! Package

2.1 准备URDF package 首先我们要准备一个机械臂的urdf&#xff0c;如果你已有URDF&#xff0c;可以使用自己的urdf模型。若手头没有现成的URDF&#xff0c;可以从此处下载一个库卡LWR简化模型URDF&#xff0c;这是一个固定底座7自由度的机械臂。 从该连接处依次进入examples/s…

穴位大揭秘:使用「人体穴位图解」,轻松学会精准按摩技巧

软件介绍 人体穴位图解是一款免费无广提供人体穴位图解、人体经络病症及穴位图表的应用&#xff0c;采用目前流行的Flutter框架开发&#xff0c;遍布肺、肠、脾、胃、心、肝等部位。该应用所有资料均来自相关公共医学文献&#xff0c;具有一定的参考价值&#xff0c;通过图文介…

c++算法练习(3)石头剪刀布、输出亲朋字符串、配对碱基对、标准库的字符替换、密码翻译

#include <string>vector<string>results;results.push_bask(string1);for(const auto &result :result)//字符串可以用下表访问string myString;string.length()ss.replace(ss,find(A),1,T);char operator()(char ch) const class SomeClass {private:int mem…

SpringBoot二手车交易管理系统-计算机毕业设计源码02893

目 录 摘要 1 绪论 1.1 选题背景与意义 1.2开发现状 1.3论文结构与章节安排 2 二手车交易管理系统系统分析 2.1 可行性分析 2.1.1 技术可行性分析 2.1.2 经济可行性分析 2.1.3 法律可行性分析 2.2 系统功能分析 2.2.1 功能性分析 2.2.2 非功能性分析 2.3 系统用例…

Linux scp命令 | 菜鸟教程-从本地复制到远程/从远程复制到本地

目录 Linux scp命令 语法 实例 1、从本地复制到远程 2、从远程复制到本地 说明 文章来源&#xff1a;Linux scp命令 | 菜鸟教程 Linux scp命令 nux scp 命令用于 Linux 之间复制文件和目录。 scp 是 secure copy 的缩写, scp 是 linux 系统下基于 ssh 登陆进行安全的远…

linux node vue3 部署手册

第一步&#xff1a;在linux 系统中安装node 1、在网址&#xff1a;https://nodejs.org/dist/ 下载对应版本的安装包。 2、解压缩下载的压缩包到任意位置&#xff0c;推荐home下。 样例路径为&#xff1a;/home/syl/node-v20.17.0-linux-x64.tar.xz 样例&#xff1a; tar -xv…

文件外发记录监控 | 公司文档外发如何跟踪数据流向?6大策略让文件不再滥发泄密! (2024全面解读)

信息化浪潮&#xff0c;公司文档的安全管理犹如一场没有硝烟的战争。 每一个文件的外发&#xff0c;都可能成为信息泄露的缺口&#xff0c;影响企业的核心竞争力。 如何有效地监控文件外发记录&#xff0c;跟踪数据流向&#xff0c;成为企业亟需解决的问题。 本文将全面解读六…

如何在 uniapp 中实现图形验证码

全篇大概2000 字&#xff08;含代码&#xff09;&#xff0c;建议阅读时间10分钟。 什么是图形验证码&#xff1f; 图形验证码&#xff08;也称为图片验证码或验证码图像&#xff09;通常用于防止机器人自动提交表单&#xff0c;确保用户是人工操作。 一、需求 我们希望在一个…

机器学习—例子:图像识别

在上篇文章中&#xff0c;在一个需求预测示例中看到了神经网络是如何工作的&#xff0c;那么如何将类似类型的想法应用于计算机视觉应用程序。 如果你正在开发人脸识别应用程序&#xff0c;让我们深入研究一下。假设一个神经网络将这样的图片作为输入&#xff0c;并输出图片中…

别再被多线程搞晕了!一篇文章轻松搞懂 Linux 多线程同步!

前言 大家有没有遇到过&#xff0c;代码跑着跑着&#xff0c;线程突然抢资源抢疯了&#xff1f;其实&#xff0c;这都是“多线程同步”在作怪。多线程同步是个老生常谈的话题&#xff0c;可每次真正要处理时还是让人头疼。这篇文章&#xff0c;带你从头到尾掌握 Linux 的多线程…

华为OD机试真题-推荐多样性

题目描述 推荐多样性需要从多个列表中选择元素&#xff0c;一次性要返回N屏数据&#xff08;窗口数量&#xff09;&#xff0c;每屏展示K个元素&#xff08;窗口大小&#xff09;&#xff0c;选择策略&#xff1a; 各个列表元素需要做穿插处理&#xff0c;即先从第一个列表中为…

HTML、CSS 和 JavaScript 在网页设计方面的介绍

关于 HTML、CSS 和 JavaScript 在网页设计方面的介绍: HTML(超文本标记语言 - HyperText Markup Language) 结构基础:HTML 是网页内容的骨架。它通过一系列的标签来定义网页中的各种元素,比如 <html> 标签是整个页面的根标签,<head> 标签用于包含页面的元信…

对于用户密码的加密

这篇文章也是在做项目的时候使用到的内容&#xff0c;来做成一篇博客 &#xff08;一&#xff09;加密是什么&#xff1f; 我们在https中也说到了加密&#xff0c;因为https就是http加密后的产物&#xff0c;当时又说到了运营商劫持&#xff0c;然后引出加密&#xff0c;然后加…

Hive数据库操作语法

数据类型 内部表和外部表 内部表 &#xff08;CREATE TABLE table_name ......&#xff09;未被external关键字修饰的即是内部表&#xff0c; 即普通表。 内部表又称管理表,内部表数据存储的位置由hive.metastore.warehouse.dir参数决定&#xff08;默认&#xff1a;/user/h…

车载通信架构 --- PNC、UB与信号的关系

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 所有人的看法和评价都是暂时的,只有自己的经历是伴随一生的,几乎所有的担忧和畏惧,都是来源于自己的想象,只有你真的去做了,才会发现有多快乐。…

CLIP-Driven Universal Model for Organ Segmentation and Tumor Detection论文解读和实验复现

背景 医学图像数据集的增长&#xff1a;随着公开的医学图像数据集数量的增加&#xff0c;自动化器官分割和肿瘤检测技术得到了显著发展。数据集的局限性&#xff1a;尽管数据集数量增加&#xff0c;但每个数据集通常规模较小&#xff0c;且存在部分标注问题。这意味着不是所有…

如何使用Varjo直接观看Blender内容

最近&#xff0c;开源的3D建模程序Blender为Varjo提供了出色的OpenXR支持&#xff0c;包括四视图和凹进渲染扩展。但是在Blender中&#xff0c;默认不启用VR场景检查。要开始使用VR场景检查&#xff0c;只需遵循以下步骤&#xff1a; 1. 下载并安装Blender 2.启用Blender VR场景…