C++中的虚函数

前言

本篇文章讲述C++的虚函数

定义

在C++语言中,基类将类型相关的函数和派生类不做改变直接继承的函数区分开来。对于有些函数,基类希望派生类各自定义适合自身的版本。那么基类就会将这些函数标记为virtual,这些被标记的函数就是虚函数。
下面这就是一个虚函数在代码中的定义,和普通的函数一样,只不过前面添加了关键字virtual

class A_CLASS
{
public:virtual void print() {std::cout << "invoke A_CLASS virtual function::printf" << std::endl;}
};

**如果派生类想要重新定义虚函数,派生类需要在自己的类中重新声明虚函数。**声明的时候需要注意两点:

  • 可以在前面添加virtual关键字,也可以不添加,建议添加
  • 可以在函数声明的结尾添加override关键字,也可以添加,建议添加
  • virtual只能出现在类内部的函数声明之前而不能用于类外部的函数定义
  • 如果一个基类把函数声明为虚函数,则在派生类中该函数默认也是虚函数

先看第一条,为什么建议添加,在我们阅读代码的时候,明确一个函数是不是虚函数对我们理解代码结构很有帮助,尤其是类层级变多以后,这条只是从提高代码的可读性角度来看。

对于第二条,我们先看下面的代码:

#include <iostream>
class A_CLASS
{
public:virtual void print()   {std::cout << "invoke A_CLASS virtual function::printf" << std::endl;}
};class B_CLASS:public A_CLASS
{
public:virtual void prnit() {std::cout << "invoke B_CLASS virtual function::printf" << std::endl;}
};

对于上面代码,编译是没有问题,但是使用下面的调用

int main(int argc, const char* argv[])
{A_CLASS* c = new B_CLASS();c->print();return 0;
}

打印的结果却是

invoke A_CLASS virtual function::printf

根据多态的特性,我们应该是想调用B_CLASS的print方法,但是在B_CLASS中,我们不小心把print方法写成了prnit。编译没有问题,但是不是我们期望的结果,如果我们在后边添加关键字override。
override关键字强调我们的方法要重新实现基类的虚函数,如果基类没找到该函数,编译器会报错。override关键字能让我们预防上面出现的漏洞

动态绑定和静态绑定

对于C++的函数调用,有两种方式:

  • 静态绑定:就是编译器在编译代码阶段就能确定当前的函数调用是哪一个,并且知道函数在内存中的具体位置,所以编译器会直接把内存的位置传递给调用指令。这种调用方式叫做静态绑定,静态绑定效率是最高的,没有中间商。
  • 动态绑定:在编译阶段编译器不知道具体执行的函数的内存位置,直到代码运行到这里的时候才能确定,这种调用方式叫做动态绑定。,编译器对于动态绑定的函数,无法直接指定调用函数的内存位置给函数调用指令

在C++语言中,当我们使用基类的引用或者指针调用一个虚函数时将发生动态绑定。动态绑定是多态得以实现的基础

动态绑定的原理

知道了动态绑定和静态绑定的定义,现在我们来研究一下动态绑定的实现原理
我们知道,一个函数在内存中其实是一系列的字节数据,用汇编表示就是一系列的汇编指令,我们执行一个函数的步骤如下:

  • 将函数需要的参数传递给寄存器或者栈空间
  • 然后使用call指令跳转到函数的内存地址
  • 然后开始执行函数
  • 执行函数后使用ret指令返回执行前的位置

知道函数的执行步骤,我们看一下一个类的虚函数的特点

  • 首先,虚函数的实现代码在内存是已知的,这点跟普通的函数是一样的
  • 然后,虚函数的参数是已知的,这样编译器可以提前传递参数数据
  • 最后,就剩下函数的跳转了,这也是多态实现的最重要的地方

一个类,如果有虚函数存在的话,编译器会为这个类分配一块内存,专门用来放虚函数实现代码在内存的位置,你可以把这块内存理解为指针的数组。这块内存被称为虚函数表,简称vtbl,全称virtual table

每个类都会有一块这样的内存,基类和派生类分别有自己的虚函数表

对于一个类创建的实例,所有的实例都会包含一个指针,这个指针指向上面说的那块内存。这个指针叫做虚函数表指针,简称vptr,全称virtual pointer。一般来说,虚函数表指针在类实例的最前面。

我们看一个实例:

#include <iostream>
class A_CLASS
{
public:virtual void print1() {}virtual void print()   {std::cout << "invoke A_CLASS virtual function::printf" << std::endl;}
};class B_CLASS:public A_CLASS
{
public:virtual void print() override {std::cout << "invoke B_CLASS virtual function::printf" << std::endl;}
};int main(int argc, const char* argv[])
{A_CLASS* c = new B_CLASS();std::cout << "c.size::" << sizeof(*c) << std::endl;c->print();return 0;
}

打印B_CLASS实例的大小,发现有8个字节,我们猜测这个8字节的值正是虚函数表指针的大小,我们在这里加个断点,运行一下,鼠标停在c变量上,在出现的提示区域右键,选择添加监视,可以看到类实例的内容如下:
在这里插入图片描述

这印证了我们的猜测。
在c->print();这一行打个断点,继续执行到这里,然后打开反汇编窗口,我们可以看到关键的四行代码:

00007FF719731E7A  mov         rax,qword ptr [c]  
00007FF719731E7E  mov         rax,qword ptr [rax]  
00007FF719731E81  mov         rcx,qword ptr [c]  
00007FF719731E85  call        qword ptr [rax+8]

我们分析一下这四行代码:

  1. 将c指针的值传递给rax,c指针的值就是B_CLASS类实例在内存的位置,我们从监视窗口看到了,值为0x00000171286a23f0,这块内存目前保存了虚函数表的位置,我们可以在内存窗口输入0x00000171286a23f0查询一下,结果如图,跟监视窗口的虚函数表指针是一样的:
    在这里插入图片描述

  2. 将rax地址中保存的值赋值给rax,也就是经过这一步,rax保存的值变成了虚函数表在内存的位置,经过这一步rax的值由0x00000171286a23f0变为00007FF71973BC80

  3. 将c指针的值传递给rcx,这一步是因为我们调用虚函数的时候需要传递默认参数this,这个默认参数是第一个参数,保存在寄存器rcx中,因为我们的虚函数没有别的参数了,所以这里就传递这一个值。

  4. call [rax+8]中的值,为什么是rax+8呢,因为B_CLASS的虚函数表有两个虚函数,一个是在A_CLASS中定义的print1,另一个是自己重定义的print。我们调用的是print,所以要往后移动8个字节才能定位到保存print函数的指针位置。

经过上面的分析和查看汇编代码我们知道了动态绑定发生的地方:
动态绑定就是发生在虚函数表指针那里。不同的类实例这个虚函数表指针指向的位置不一样,所以才能调用不同的虚函数,这,就是多态

两者的比较

我们上面看到了动态绑定的执行过程,现在看一下静态绑定的执行过程,将上面main中的代码修改一下:

int main(int argc, const char* argv[])
{B_CLASS b;b.print();return 0;
}

还是在b.print();打一个断点,执行到断点之后,查看反汇编界面,显示如下:

00007FF711EA1E25  lea         rcx,[b]  
00007FF711EA1E29  call        B_CLASS::print (07FF711EA115Eh)

可以看到,没有取地址的操作,就两步:

  • 获取this给rcx
  • 调用

静态的函数调用确实比动态调用效率高,但是失去了动态调用的多样性。

虚函数表

这一小节,我们讲一下虚函数表中虚函数的排列,其实从上一节已经看到了,虚函数调用时,在虚函数表中的偏移是个常量,也就是说在编译阶段,编译器已经确定了虚函数在虚函数表中的偏移位置
既然虚函数的位置在虚函数表中是静态的,那么在类继承的关系层次中,虚函数的布局就是明确的。看下面的例子:

class A_CLASS
{
public:virtual void print1() {}virtual void print2()   {std::cout << "invoke A_CLASS virtual function::printf" << std::endl;}virtual void print3() {}
};class B_CLASS:public A_CLASS
{
public:virtual void print2() override {std::cout << "invoke B_CLASS virtual function::printf" << std::endl;}virtual void print4() {};
};

对于B_CLASS的实例,虚函数表的虚函数布局方式应该是这样的:

print1--A_CLASS::print1
print2--B_CLASS::print1
print3--A_CLASS::print1
print4--B_CLASS::print1

对于A_CLASS的实例,虚函数表的虚函数布局方式应该是这样的:

print1--A_CLASS::print1
print2--A_CLASS::print1
print3--A_CLASS::print1

这样,每个虚函数在表中的偏移就是固定的了。

上面我们的例子是单继承的情况,C++支持多继承,对于多继承,就比较复杂了,我们修改一下上面的例子,这次我们给每个类加了一个int变量:

class A_CLASS
{
public:int a;virtual void print1() {}virtual void print2()   {std::cout << "invoke A_CLASS virtual function::printf" << std::endl;}virtual void print3() {}
};class B_CLASS
{
public:int b;virtual void print2() {std::cout << "invoke B_CLASS virtual function::printf" << std::endl;}virtual void print4() {};
};class C_CLASS :public A_CLASS,public B_CLASS
{
public:int c;virtual void print1() override { std::cout << "invoke B_CLASS virtual function::printf" << std::endl; }virtual void print4() {};virtual void print5() {};
};

C_CLASS同时继承了A_CLASS和B_CLASS,那虚函数表是怎么的呢?
对于多继承的类,虚函数表的规则如下:

  • 对于多继承的类,虚函数表不止一个,继承了几个类,就有几个虚函数表
  • 对于多继承的类,内存的排布如下,以上面的例子为例
    A_CLASS虚函数表指针--8字节
    int a--4字节,对齐到8字节
    B_CLASS虚函数表指针--8字节
    int b--4字节,对齐到8字节
    int c--4字节,对齐到8字节
    
  • 对于B_CLASS* b = new C_CLASS();编译器会对b的指针进行调整,使其指向
    B_CLASS虚函数表指针
    

这个位置。编译器通过这样的调整,让所有状态的类实例具有统一的调用方式。

使用虚函数需要注意的事项

虚析构函数

基类如果包含虚函数通常都应该定义一个虚析构函数,即使该函数不执行任何操作也是如此

比如C_CLASS继承的A_CLASS,如果我们没有将A_CLASS的析构函数设置为虚函数的话,我们现在在派生类C_CLASS的某个方法分配了一块内存,在C_CLASS的析构函数中进行的释放,这没有问题。但是我们接着进行下面的操作:

A_CLASS* a = new C_CLASS();
a->b();//分配了一块内存
delete a;

那么我们通过delete a的方式释放内存的时候,不会调用派生类C_CLASS的析构函数,因为不是虚函数,也就不会动态绑定,执行静态绑定会调用A_CLASS的析构函数,这样,之前分配的内存就泄漏了。

虚函数的返回值

如果一个基类定义的虚函数返回值是自身的引用或者指针,派生类重写虚函数的时候返回值可以是派生类自身的引用或指针。

虚函数的默认实参

如果某次虚函数的调用使用了默认实参,则该实参的值由对象的静态类型决定。一般对于这种情况,基类和派生类的默认值设置成一样。

为什么会这样呢?
其实通过前面的分析,这一点已经很明确了,还记得前面的虚函数调用代码吗?

00007FF719731E7A  mov         rax,qword ptr [c]  
00007FF719731E7E  mov         rax,qword ptr [rax]  
00007FF719731E81  mov         rcx,qword ptr [c]  
00007FF719731E85  call        qword ptr [rax+8]

我们说过,动态绑定只发生在虚函数表指针那里。编译器在编译的时候,已经准备好传递参数了,如果是默认实参,会把该实参的默认值传递到寄存器或者栈空间。但是这个时候是不知道具体的实际类型的,只能把当前的静态类型的值传递过来。

虚函数调用虚函数

如果我们想要在派生类的虚函数中调用基类的虚函数,可以使用作用域运算符实现,否则将变成无限递归

纯虚函数

如果我们在一个虚函数的声明结尾添加=0,那么这个虚函数会被定义为纯虚函数,纯虚函数有以下特点:

  • 纯虚函数所在的类被称为抽象类,抽象类不能实例化
  • 如果继承抽象类的派生类没有重新实现虚函数并且取消定义为纯虚函数,该派生类还是抽象类。

可用虚函数

一个对象,引用或者指针的静态类型决定了该对象哪些成员是可见的,当然也包括哪些虚函数是可调用的

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

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

相关文章

2024年如何使用WordPress构建克隆Udemy市场

您想创建像 Udemy 这样的学习管理 (LMS) 网站吗&#xff1f;最好的学习管理系统工具LifterLMS将帮助您制作像Udemy市场这样的 LMS 网站。 目录 Udemy市场是什么&#xff1f; 创建 Udemy 克隆所需的几项强制性技术&#xff1a; 步骤 1) 注册您的域名 步骤 2) 获取虚拟主…

springboot git配置文件自动刷新失败问题排查

http://{ip}:{port}/refresh 说明&#xff1a;springBoot版本是1.5.9&#xff0c;接口路径与2.x&#xff0c;不同 路径区别&#xff1a;/refresh VS /actuator/refresh 用postman调用refresh接口刷新git配置&#xff0c;报错如下&#xff0c;没有权限 在服务本地启动&#…

微信私密朋友圈被吐槽有BUG

日前&#xff0c;大量网友在各社交媒体上讨论微信私密朋友圈出现 Bug 的话题&#xff0c;起因是跨年期间一个网友发布了一条”私密朋友圈&#xff0c;但不一会就收到朋友发来的信息&#xff0c;”又偷偷发朋友圈了&#xff1f;“&#xff0c;估计此时网友可能已经”寒毛四起、汗…

D3篇之色卡

学习传送门&#xff1a;Sequential scales | D3 by Observable 1.scaleSequential(domain, interpolator)&#xff08;连续比例尺&#xff09; 是一种在D3.js中用于将一个范围内的连续值射到另一个范围内的连续值的方法。该比例尺通常用于将数值型数据映射到图表元素的属性上…

jenkins忘记密码后的操作

1、先停止 jenkins 服务 systemctl stop jenkins 关闭Jenkins服务 或者杀掉进程 ps -ef | grep jenkins &#xff5c;awk {print $2} | grep -v "grep" | xargs kill -9 2、找到 config.xml 文件 find /root -name config.xml3、备份config.xml文件 cp /root/.jen…

Java面试——框架篇

1、Spring框架中的单例bean是线程安全的吗&#xff1f; 所谓单例就是所有的请求都用一个对象来处理&#xff0c;而多例则指每个请求用一个新的对象来处理。 结论&#xff1a;线程不安全。 Spring框架中有一个Scope注解&#xff0c;默认的值就是singleton&#xff0c;单例的。一…

【STM32】STM32学习笔记-USART串口外设(26)

00. 目录 文章目录 00. 目录01. 串口简介02. 串口协议03. USART简介04. USART框图05. USART基本结构06. 数据帧07. 起始位侦测08. 数据采样09. 波特率发生器10. 附录 01. 串口简介 串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式&#xff0c;因为它简单便…

基于FPGA的RLC测试仪

1. 系统设计 以FPGA为控制器&#xff0c;实现RLC(电阻、电容、电感)的检测&#xff0c;其测量电路如下&#xff1a;

性能优化-OpenMP基础教程(四)-Android上运行OpenMP

本文主要介绍如何在一个常规的Android手机上调试OpenMP程序&#xff0c;包括Android NDK的环境配置和使用JNI编写一个OpenMP程序运行在Android手机中。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;高性能&#…

2023 年精选:每个 DevOps 团队都应该了解的 5 种微服务设计模式

微服务彻底改变了应用程序开发世界&#xff0c;将大型整体系统分解为更小、更易于管理的组件。这种架构风格的特点是独立、松散耦合的服务&#xff0c;带来了从可扩展性、模块化到更高的灵活性等众多优势。 DevOps 团队如何最好地利用这种方法来实现最高效率&#xff1f;答案在…

vue中短时间内多次点击同一个按钮会向后端发送多个请求

在vue中&#xff0c;我们可能会遇到以下问题&#xff1a; 我们有两种方法解决&#xff1a; &#xff08;1&#xff09;可以通过设置一个标志位来防止用户在短时间内多次点击同一个按钮导致向后端发送多个请求。具体实现方式如下&#xff1a; 定义一个 isFetching变量来表示当…

欧盟食品接触材料测试1935/2004/EC介绍

欧盟官方公报(OJ)发布与食品接触的塑料制品法规(EU)10/2011的修订法规(EU)2017/752。欧盟食品级塑料法规从(EU)10/2011发布以来&#xff0c;已历经7次修订&#xff0c;前5次的修订版本均是针对EU10/2011法规里的附录1授权物质清单进行修订。第6次修订法规(EU)2016/1416澄清和纠…

寻找两个相交链表的相交节点

分析&#xff1a; 如图所示&#xff0c; A 长度为mkB长度为nk张三&#xff0c;李四两人分别从A和B的起始点相同速度出发&#xff0c;无论谁到达终点时&#xff0c;都从另一条队列的起点再次出发。假定起始&#xff0c;张三沿着A走&#xff0c;李四沿着B走。当李四到达终点后&a…

thinkadmin列表多图点击放大

头像展示 原型 {field: images, title: 图片, align: center, minWidth: 200,

云原生技术专题 | 解密2023年云原生的安全优化升级,告别高危漏洞、与数据泄露说“再见”(安全管控篇)

背景介绍 2023年&#xff0c;我们见证了科技领域的蓬勃发展&#xff0c;每一次技术革新都为我们带来了广阔的发展前景。作为后端开发者&#xff0c;我们深受其影响&#xff0c;不断迈向未来。 随着数字化浪潮的席卷&#xff0c;各种架构设计理念相互交汇&#xff0c;共同塑造了…

73应急响应-Web分析phpjavaWeb自动化工具

我感觉学完渗透自然就会应急响应&#xff0c;之前又发过应急响应的文章 应急响应笔记就开始比较潦草 应急响应基础知识 应急响应流程 保护阶段&#xff08;断网&#xff0c;避免继续渗透&#xff1b;备份&#xff09;&#xff0c;分析阶段&#xff08;分析攻击行为&#xf…

二 数据查询

1、实验目的 理解SQL成熟设计基本规范&#xff0c;熟练运用SQL语言实现数据基本查询&#xff0c;包括但表查询、分组统计查询和连接查询。 2、实验内容及要求 针对数据库设计各种单表查询SQL语句、分组统计查询语句&#xff1b;设计单个表针对自身的连接查询&#xff0c;设计…

WiFi6工业网关能为工业物联网带来哪些改进?

WiFi 6&#xff08; 802.11ax&#xff09;比其前身WiFi 5&#xff08;802.11ac&#xff09;带来了多项改进&#xff0c;例如更快的通信速率、更大的带宽容量、在多设备连入时更稳定的性能、更大的链接范围、增强的安全性以及更好地支持物联网工作负载等&#xff0c;本篇就为大家…

国标GB28181视频监控EasyCVR平台:视频集中录制存储/云端录像功能及操作介绍

安防视频监控系统EasyCVR视频综合管理平台&#xff0c;采用了开放式的网络结构&#xff0c;可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级联、磁盘阵列存储、视频集中存储、云存储等丰富的视频能力&#xff0c;同时还具备权限管理、设…

C++ 软件常用分析工具及项目实战问题分析案例集锦

目录 1、库依赖关系查看工具Dependency Walker 2、GDI对象查看工具GDIview 3、PE信息查看工具PeViewer/MiTeC EXE Explorer 4、进程信息查看工具Process Explorer 5、进程监控工具Process Monitor 6、API函数调用监测工具API Monitor C软件异常排查从入门到精通系列教程&…