当前位置: 首页 > news >正文

【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)



快乐的流畅:个人主页


个人专栏:《C语言》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

  • 一、虚函数与重写
    • 1.1 虚函数
    • 1.2 虚函数的重写
    • 1.3 重写的特例
    • 1.4 final和override(C++11)
    • 1.5 重载、重写(覆盖)、重定义(隐藏)的对比
  • 二、多态的概念及定义
    • 2.1 多态的概念
    • 2.2 多态的定义
  • 三、抽象类
    • 3.1 纯虚函数
    • 3.2 抽象类的概念
    • 3.3 接口继承与实现继承
  • 四、多态的原理
    • 4.1 虚函数表
    • 4.2 虚函数表的打印
    • 4.3 单继承下的虚函数表
      • 4.3.1 一对一
      • 4.3.2 多对一
      • 4.3.3 一对多
    • 4.4 多继承下的虚函数表
    • 4.5 多态的原理
    • 4.6 静态绑定与动态绑定
    • 4.7 菱形虚拟继承下的虚函数表

一、虚函数与重写

1.1 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

1.2 虚函数的重写

虚函数的重写,又称覆盖。派生类有一个函数名、参数、返回值与基类虚函数相同的虚函数,则称派生类的虚函数重写了基类的虚函数。

class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
}

同时,虚函数重写,其意义在于继承函数接口,重写函数定义

1.3 重写的特例

  1. 派生类要重写的虚函数,可以不用加virtual关键字(不推荐使用)
class Person
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person
{
public:void BuyTicket() { cout << "买票-半价" << endl; }
}

原因:由于继承,派生类的同名函数继承了基类虚函数的特性。

  1. 协变
    派生类和基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类返回派生类对象的指针或引用。
class A{};
class B : public A {};class Person
{
public:virtual A* f() {return new A;}
};
class Student : public Person
{
public:virtual B* f() {return new B;}
};
  1. 析构函数的重写
    如果基类的析构函数为虚函数,那么只要派生类的析构函数定义,便构成重写。
class Person
{
public:virtual ~Person() {cout << "~Person()" << endl;}
};class Student : public Person
{
public:virtual ~Student() { cout << "~Student()" << endl; }
};

原因:编译器此时做了特殊处理,将基类和派生类的析构函数名,都改为destructor,因此构成重写。

那么为什么要这么处理呢?请看下面代码:

int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

原因:只有这样处理,构成多态,才能正确调用各自的析构函数。

1.4 final和override(C++11)

  1. final:可以修饰变量、函数和类。
    对于变量,确保初始化后不能被修改
    对于函数,确保不能被子类重写
    对于类,确保不能被继承
class Car
{
public:virtual void Drive() final {}
};class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒适" << endl;}
};

加上final,以上代码会编译报错。

  1. override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car
{
public:virtual void Drive() {}
};class Benz :public Car
{
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

1.5 重载、重写(覆盖)、重定义(隐藏)的对比

二、多态的概念及定义

2.1 多态的概念

多态,顾名思义,即多种形态。具体来说,就是不同对象执行同一行为而产生不同的结果

比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

2.2 多态的定义

多态,是在不同继承关系的类对象,去调用同一函数,产生不同的行为

比如:Student继承了Person。Person对象买票全价,Student对象买票半价。

构成多态需要两个条件:

  1. 通过父类的指针或引用调用
  2. 被调用的必须是虚函数,并且虚函数必须重写

三、抽象类

3.1 纯虚函数

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

class Car
{
public:virtual void Drive() = 0;
};

3.2 抽象类的概念

包含纯虚函数的类叫做抽象类,也叫接口类。

抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象

class Car
{
public:virtual void Drive() = 0;
};class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};void Test()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}

意义:纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

3.3 接口继承与实现继承

普通函数的继承,是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

虚函数的继承,是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。

综上所述,虚函数就是为多态而生的,如果不实现多态,不要把函数定义成虚函数。

四、多态的原理

4.1 虚函数表

先来看一道题:32位平台下,sizeof(Base)是多少?

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};

正确答案是8byte!是不是很诧异?

其实,Base类里面还有一个隐藏的指针,称为虚函数表指针(简称虚表指针)。


经过观察发现,其类型为void**,并且(与平台有关,vs平台下)位于对象的最上方。

而且,这个指针指向了一张表,称为虚函数表(简称虚表)。虚函数表,是一个函数指针数组,里面存储了该类中虚函数的指针。

4.2 虚函数表的打印

由于监视窗口会隐藏一些真实的信息,并且观察起来不太直观和方便,所以我们写一个函数专门打印虚函数表,以便观察和检验。

typedef void(*VFT_PTR)();void PrintVFTable(VFT_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]: %p-> ", i, table[i]);VFT_PTR f = table[i];f();}cout << endl;
}

细节:

  1. 由于函数指针不太直观,先typedef重命名一下
  2. 传参传入二级指针,也就是虚表指针
  3. 这里利用一个性质:虚函数表以nullptr结尾,以作标识(vs平台)

至于如何取出虚表指针,这也是需要一定的技巧。先给出下面分析要用的main函数

int main()
{Base b;Derive d;PrintVFTable(*(VFT_PTR**)&b);PrintVFTable(*(VFT_PTR**)&d);return 0;
}

细节:

  1. 利用性质:虚表指针在对象的开头(vs平台)
  2. 取出对象地址,再强转为VFT_PTR**,这样解引用就可以直接获取虚表指针大小的内容

需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

4.3 单继承下的虚函数表

4.3.1 一对一

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}
private:int _b = 1;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};

运行结果:

4.3.2 多对一

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}
private:int _b = 1;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};

运行结果:

4.3.3 一对多

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}
private:int _b = 1;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func4(){cout << "Derive::Func4()" << endl;}
private:int _d = 2;
};

运行结果:


综上三种情况:

  1. 基类的虚函数表,(按照声明顺序)存储基类中的虚函数指针。
  2. 派生类的虚函数表,先将基类的虚函数表拷贝过来,再对被重写的虚函数覆盖为派生类的虚函数,最后在末尾加上派生类新增的虚函数。

这里也体现了为什么重写又称覆盖,重写是语法层的叫法,覆盖是原理层的叫法

4.4 多继承下的虚函数表

那么,有了上面单继承下的虚函数表的基础,我们再来看看多继承虚函数表有哪些变化吧。

class Base1
{
public:virtual void func1(){cout << "Base1:func1()" << endl;}virtual void func2(){cout << "Base1:func2()" << endl;}
private:int _b1;
};class Base2
{
public:virtual void func1(){cout << "Base2:func1()" << endl;}virtual void func2(){cout << "Base2:func2()" << endl;}
private:int _b2;
};class Derive :public Base1, public Base2
{
public:virtual void func1(){cout << "Derive::func1()" << endl;}virtual void func3(){cout << "Derive::func3()" << endl;}
private:int _d1;
};

我们先来看看监视窗口:

我们可以发现,多继承下的派生类对象,将两个基类的虚表都继承了过来,所以后续打印时要注意打印两份虚表。

这里需要找到派生类对象中两个虚表指针的位置,可以用到切片的技巧,实现指针自动定位。

int main()
{Base1 b1;Base2 b2;Derive d;Base1* p1 = &d;Base2* p2 = &d;PrintVFTable(*(VFT_PTR**)&b1);PrintVFTable(*(VFT_PTR**)&b2);PrintVFTable(*(VFT_PTR**)p1);PrintVFTable(*(VFT_PTR**)p2);return 0;
}

运行结果:

结论:

  1. 派生类分别将各个基类的虚表拷贝过来,再对被重写的虚函数进行覆盖
  2. 唯一不同的,是派生类新增的虚函数,是放在第一个继承的基类部分虚表的最后。

4.5 多态的原理

讲了这么多虚函数表的内容,所以这跟多态的原理有什么关系呢?我们再来回看一开始这张多态调用分析图:

  1. 为什么要使用父类的指针或引用来调用?因为子类的虚表存储在继承的父类部分,这样才能统一调用父类子类各自的虚表。

  2. 为什么被调用的虚函数必须重写?因为这是一种接口继承,也是你要实现多态的根本目的。在重写了虚函数的实现后,调用时在父类子类各自的虚表查找各自不同实现的虚函数,才能构成多态。

4.6 静态绑定与动态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

所以,满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

4.7 菱形虚拟继承下的虚函数表

这里已经属于考试不考,实际中不常用的范围了,有兴趣可以看看~

class A
{
public:virtual void func1(){}int _a;
};class B :virtual public A
{
public:virtual void func1(){}virtual void func2(){}int _b;
};class C :virtual public A
{
public:virtual void func1(){}virtual void func3(){}int _c;
};class D :public B, public C
{
public:virtual void func1(){}virtual void func4(){}int _d;
};int main()
{D d;d._b = 1;d._c = 2;d._d = 3;d._a = 4;return 0;
}

虚表(虚函数表)存储虚函数地址

虚基表存储偏移量


细节:

  1. D类中必须重写func1,避免B和C类多重继承时重写的歧义性
  2. 虚拟继承中,重写的func1位于A部分虚表,而B和C类中未重写的虚函数,分别位于B和C部分的虚表
  3. D类中新增的虚函数,放在第一个继承类部分的虚表(即B部分虚表)
  4. 虚基表中(总共两个位置),第一位置记录距离虚表指针的偏移量,第二位置记录距离A部分的偏移量

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。


真诚点赞,手有余香

相关文章:

【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C语言》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 一、虚函数与重写1.1 虚函数1.2 虚函数的重写1.3 重写的特例1.4 final和override&#xff08;C11&#xff09;1.…...

如何在Windows系统安装Node.js环境并制作html页面发布公网远程访问?

文章目录 前言1.安装Node.js环境2.创建node.js服务3. 访问node.js 服务4.内网穿透4.1 安装配置cpolar内网穿透4.2 创建隧道映射本地端口 5.固定公网地址 前言 Node.js 是能够在服务器端运行 JavaScript 的开放源代码、跨平台运行环境。Node.js 由 OpenJS Foundation&#xff0…...

C#,数值计算,希尔伯特矩阵(Hilbert Matrix)的算法与源代码

Hilbert, David (1862-1943) 1 希尔伯特(Hilbert) 德国数学家,在《几何学基础》中提出了第一套严格的几何公理(1899年)。他还证明了自己的系统是自洽的。他发明了一条简单的空间填充曲线,即埃里克魏斯汀的数学世界,即希尔伯特曲线,埃里克魏斯汀的数学世界,并证明了不…...

【C++教程从0到1入门编程】第八篇:STL中string类的模拟实现

一、 string类的模拟实现 下面是一个列子 #include <iostream> namespace y {class string{public: //string() //无参构造函数// :_str(nullptr)//{}//string(char* str) //有参构造函数// :_str(str)//{}string():_str(new char[1]){_str[0] \0;}string(c…...

学生时期学习资源同步-1 第一学期结业考试题6

原创作者&#xff1a;田超凡&#xff08;程序员田宝宝&#xff09; 版权所有&#xff0c;引用请注明原作者&#xff0c;严禁复制转载...

迁移学习怎么用

如果想实现一个计算机视觉应用&#xff0c;而不想从零开始训练权重&#xff0c;比方从随机初始化开始训练&#xff0c;更快的方式是下载已经训练好权重的网络结构&#xff0c;把这个作为预训练&#xff0c;迁移到你感兴趣的新任务上。ImageNet、PASCAL等等数据库已经公开在线。…...

医疗手持智能终端读取条码二维码的难点有哪些?

在医疗科技行业信息化的大潮中&#xff0c;医疗手持式智能终端的应用越发普及&#xff0c;医疗手持式智能终端对条码二维码技术应用显得尤为关键&#xff0c;作为信息朔源载体的条码二维码读取方面&#xff0c;在实际应用中却面临着诸多问题&#xff0c;我们该如何应对&#xf…...

Python小设计

1. 五个PPT上的界面打印【print、input函数】 &#xff08;1&#xff09;英雄商城登陆界面 print(英雄联盟商城登录界面 ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~1. 用户登录2. 新用户注册3. 退出系统 ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~ * ~…...

今日讲讲父子传值~

今天来讲讲父子传值中的几种方法~ 项目中往往会把一些常用的公共代码抽离出来&#xff0c;写成一个子组件。或者在一个页面中的代码太多&#xff0c;可以根据功能的不同抽离出相关代码写成子组件&#xff0c;这样代码结构会更加简洁明了&#xff0c;后续维护更加方便。…...

三、HarmonyOS 应用开发入门之运行Hello World

目录 1、课程对象 1.1、有移动端开发经验 1.2、无移动端开发经验 1.3、对 HarmonyOS 感兴趣 2、DevEco Studio 的使用 2.1、DevEco Studio 的关键特性 智能代码编辑 低代码开发 多段双向实时预览 多端模拟仿真 2.2、安装配置 DevEco Studio 2.2.1、官网开发工具下载地…...

国科大网络行为学导论代码作业--更新中

一、Xray安装 参考自&#xff1a;Xray的安装与使用&#xff08;超详细&#xff09;_xray使用教程-CSDN博客 下载网址&#xff1a;Releases chaitin/xray GitHub 解压 双击安装 生成证书 cd到xray目录&#xff0c;生成证书 复制链接 然后cd到xray目录 .\xray_windows_amd6…...

JAVA后端开发面试基础知识(九)——SpringBoot

启动原理 SpringBoot启动非常简单,因其内置了Tomcat,所以只需要通过下面几种方式启动即可: @SpringBootApplication(scanBasePackages = {"cn.dark"}) public class SpringbootDemo {public static void main(String[] args) {// 第一种SpringApplication.run(S…...

C#调用Halcon出现尝试读取或写入受保护的内存,这通常指示其他内存已损坏。System.AccessViolationException

一、现象 在C#中调用Halcon&#xff0c;出现异常提示&#xff1a;尝试读取或写入受保护的内存,这通常指示其他内存已损坏。System.AccessViolationException 二、原因 多个线程同时访问Halcon中的某个公共变量&#xff0c;导致程序报错 三、测试 3.1 Halcon代码 其中tsp_width…...

ts基础知识

1. any 类型&#xff0c;unknown 类型&#xff0c;never 类型 TypeScript 有两个“顶层类型”&#xff08;any和unknown&#xff09;&#xff0c;但是“底层类型”只有never唯一一个 1、any 1.1 基本含义 any 类型表示没有任何限制&#xff0c;该类型的变量可以赋予任意类型的…...

KLayout Python Script ------ 绘制1个 Box 物体

KLayout Python Script ------ 绘制两个 Box 物体 引言正文引言 本人通常使用 IPKISS 进行版图绘制,然而很多时候,IPKISS 的功能十分鸡肋。因此,萌生了一种自己写绘制软件的想法,因为 IPKISS 绘制的版图最终也是使用 KLayout 来呈现的,因此,再研究了 KLayout 提供的 API…...

c# 编辑、删除一条数据

1、编辑数据 [HttpPost] public MessageModel<string> Put([FromBody] Dtable request) { var data new MessageModel<string>(); request.UPDATETIME DateTime.Now; if (request.ID>0) { …...

高性能服务系列【八】C10M时代,网络IO库需要重建

在目前网络上能搜索到的&#xff0c;关于网络IO模型的文章&#xff0c;基本都是关于多路复用的iocp/epoll的&#xff0c;这些技术是为了解决C10K问题而提出的解决方案。现代网卡已经普遍支持10Gb&#xff0c;100Gb也不少见&#xff0c;这些解决方案已经无法提升性能的需求。 我…...

Go语言与Rust哪一个更有发展前景?

Go语言和Rust都是目前非常受欢迎的编程语言&#xff0c;它们各自具有独特的优势和适用场景。关于哪一个更有发展前景&#xff0c;这实际上取决于多个因素&#xff0c;包括个人偏好、项目需求、社区支持以及未来技术的发展趋势等。 Go语言是由Google推出的&#xff0c;具有简洁…...

STM32使用定时器驱动电机

STM32使用定时器驱动电机 1、对定时器进行初始化配置1.1、include "encoder.c"文件 主函数 1、对定时器进行初始化配置 1.1、include "encoder.c"文件 #include "encoder.h"void TIM4_Encoder_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO…...

C语言游戏实战(4):人生重开模拟器

前言&#xff1a; 人生重开模拟器是前段时间非常火的一个小游戏&#xff0c;接下来我们将一起学习使用c语言写一个简易版的人生重开模拟器。 网页版游戏&#xff1a; 人生重开模拟器 (ytecn.com) 1.实现一个简化版的人生重开模拟器 &#xff08;1&#xff09; 游戏开始的时…...

51c自动驾驶~合集58

我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留&#xff0c;CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制&#xff08;CCA-Attention&#xff09;&#xff0c;…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)

设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile&#xff0c;新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...

vue3 定时器-定义全局方法 vue+ts

1.创建ts文件 路径&#xff1a;src/utils/timer.ts 完整代码&#xff1a; import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

自然语言处理——Transformer

自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效&#xff0c;它能挖掘数据中的时序信息以及语义信息&#xff0c;但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN&#xff0c;但是…...

3-11单元格区域边界定位(End属性)学习笔记

返回一个Range 对象&#xff0c;只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意&#xff1a;它移动的位置必须是相连的有内容的单元格…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)

前言&#xff1a; 最近在做行为检测相关的模型&#xff0c;用的是时空图卷积网络&#xff08;STGCN&#xff09;&#xff0c;但原有kinetic-400数据集数据质量较低&#xff0c;需要进行细粒度的标注&#xff0c;同时粗略搜了下已有开源工具基本都集中于图像分割这块&#xff0c…...

【Linux】Linux 系统默认的目录及作用说明

博主介绍&#xff1a;✌全网粉丝23W&#xff0c;CSDN博客专家、Java领域优质创作者&#xff0c;掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围&#xff1a;SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...

C++课设:简易日历程序(支持传统节假日 + 二十四节气 + 个人纪念日管理)

名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 专栏介绍:《编程项目实战》 目录 一、为什么要开发一个日历程序?1. 深入理解时间算法2. 练习面向对象设计3. 学习数据结构应用二、核心算法深度解析…...

Linux中《基础IO》详细介绍

目录 理解"文件"狭义理解广义理解文件操作的归类认知系统角度文件类别 回顾C文件接口打开文件写文件读文件稍作修改&#xff0c;实现简单cat命令 输出信息到显示器&#xff0c;你有哪些方法stdin & stdout & stderr打开文件的方式 系统⽂件I/O⼀种传递标志位…...