C++虚函数详解

2023-11-03

C++虚函数详解

前言

C++的特性使得我们可以使用函数继承的方法快速实现开发,而为了满足多态与泛型编程这一性质,C++允许用户使用虚函数 (virtual function) 来完成 运行时决议 这一操作,这与一般的 编译时决定 有着本质的区别。

虚函数表实现原理

虚函数的实现是由两个部分组成的,虚函数指针与虚函数表。

虚函数指针

虚函数指针 (virtual function pointer) 从本质上来说就只是一个指向函数的指针,与普通的指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。

虚函数指针是确实存在的数据类型,在一个被实例化的对象中,它总是被存放在该对象的地址首位,这种做法的目的是为了保证运行的快速性。与对象的成员不同,虚函数指针对外部是完全不可见的,除非通过直接访问地址的做法或者在DEBUG模式中,否则它是不可见的也不能被外界调用。

只有拥有虚函数的类才会拥有虚函数指针,每一个虚函数也都会对应一个虚函数指针。所以拥有虚函数的类的所有对象都会因为虚函数产生额外的开销,并且也会在一定程度上降低程序速度。与JAVA不同,C++将是否使用虚函数这一权利交给了开发者,所以开发者应该谨慎的使用。

虚函数表

此板块图片及部分内容引自于haoel大大的博客,地址:
https://blog.csdn.net/haoel/article/details/1948051

上文已经提到,每个类的实例化对象都会拥有虚函数指针并且都排列在对象的地址首部。而它们也都是按照一定的顺序组织起来的,从而构成了一种表状结构,称为虚函数表 (virtual table)

我们先来规定一个基类

class Base
{
    public:
    virtual void f(){cout<<"Base::f"<<endl;}
    virtual void g(){cout<<"Base::g"<<endl;}
    virtual void h(){cout<<"Base::h"<<endl;}
};

首先对于基类Base它的虚函数表记录的只有自己定义的虚函数

Base

接下来我们来看看子类的情况

class Derived:public Base
{
	public:
    virtual void f(){cout<<"Derived::f"<<endl;}
    virtual void g1(){cout<<"Derived::g1"<<endl;}
    virtual void h1(){cout<<"Derived::h1"<<endl;}
}

·一般覆盖继承

首先是最常见的继承,子类Derived对基类的虚函数进行覆盖继承,在这个例子中仅设计了一个函数继承的情况以此推广情况。

那么此时情况是这样的:

首先基函数的表项仍然保留,而得到正确继承的虚函数其指针将会被覆盖,而子类自己的虚函数将跟在表后。

而当多重继承的时候,表项将会增多,顺序会体现为继承的顺序,并且子函数自己的虚函数将跟在第一个表项后。


C++中一个类是公用一张虚函数表的,基类有基类的虚函数表,子类是子类的虚函数表,这极大的节省了内存

同名覆盖原则与const修饰符

如果继续深入下去的话我们将会碰见一个有趣的状况

class Base
{
public:
	virtual void func()const
	{
		cout << "Base!" << endl;
	}
};
class Derived :public Base
{
public:
	virtual void func()
	{
		cout << "Derived!" << endl;
	}
};

void show(Base& b)
{
	b.func();
}
Base base;
Derived derived;

int main()
{
	show(base);
	show(derived);
	base.func();
	derived.func();
	return 0;
}

在上述程序中我们将Base类中的虚函数base定义为const类型,我们知道const后缀的目的是为了限定该函数不对类内成员做出修改。然后我们分别声明base与derived并且通过show函数调用它们的func函数,子类传参给父类也是非常正常的一个操作,但是结果可能却令人不解:

Base!
Base!
Base!
Derived!

这里有一个很大的问题,因为当我们将derived传过去的时候并没有调用derived的虚函数!也就是说虚函数不再是多态的了。

但是的话我们只需要简单的修改任意一项:将line4结尾的const限定符去掉或者将Derived的func1后加上const便可以使一切正常。这是为什么呢?

很多其他的博客将其解释为是const符号作用的原因,但实际上这样的解释并不正确。正确的原因是:

虚函数的声明与定义要求非常严格,只有在子函数中的虚函数与父函数一模一样的时候(包括限定符)才会被认为是真正的虚函数,不然的话就只能是重载。这被称为虚函数定义的同名覆盖原则,意思是只有名称完全一样时才能完成虚函数的定义。

因此在上述的例子中,将Derived类型的子类传入show函数时,实际上类型转化为了Base,由于此时虚函数并未完成定义,Derived的func()此时仅仅是属于Derived自己的虚函数,所以在show中b并不能调用,而调用的是Base内的func。而当没有发生类型转换的时候,Base类型与Derived类型就会各自调用自己的func函数。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++虚函数详解 的相关文章

随机推荐

  • STM32CubeMX之emWin移植

    前面两章介绍了SDRAM和LTDC的使用 本篇文章将介绍emWin移植到STM32 硬件环境 STM32F429IGT6 软件环境 STM32CubeMX v5 5 0 HAL库版本 STM32CubeF4 Firmware Package
  • 什么叫泛型?有什么作用?

    作者 Java3y 链接 https www zhihu com question 272185241 answer 366129174 来源 知乎 著作权归作者所有 商业转载请联系作者获得授权 非商业转载请注明出处 一 什么是泛型 Jav
  • MySQL的B+树索引和hash索引的区别

    简述一下索引 索引是数据库表中一列或多列的值进行排序的一种数据结构 索引分为聚集索引和非聚集索引 聚集索引查询类似书的目录 快速定位查找的数据 非聚集索引查询一般需要再次回表查询一次 如果不使用索引就会进行全表扫描 还有可以进行多字段组成联
  • svn历史版本操作说明

    svn历史操作简写说明 操作的字母缩写为R 一般我们常见的操作为 A D M R A add 新增 C conflict 冲突 D delete 删除 M modify 本地已经修改 G modify and merGed 本地文件修改并且
  • centos安装gcc时出现问题:Delta RPMs disabled because /usr/bin/applydeltarpm not installed.

    解决办法 此问题安装Deltarpm包 增量 RPM 套件 即可解决 当然您也可以先使用一下命令 查看是哪个包提供applydeltarpm yum provides applydeltarpm yum install deltarpm y
  • 我见过的最糟糕代码

    作者 Mehdi Zed 在本文 将向读者展示一些作者见过的一些最糟糕的代码 除非你希望被同事和用户讨厌 否则这些 魔鬼 永远都不应该被放到人间 我们会发现 通过一些好的实践 其实很容易避免它们 01 魔鬼代码 需要改进的代码与所谓的 魔鬼
  • 2023年Node.js全网详细下载安装的最新教程

    文章目录 1 文章引言 2 下载安装 3 检查是否安装成功 4 补充说明 1 文章引言 今天准备写下载和安装vue js的博文 但安装vue js的前提是要安装node和npm 我们在安装node js时 会自动安装的npm包管理器 接下来
  • [深度学习]Part2 数据清洗和特征工程Ch06——【DeepBlue学习笔记】

    本文仅供学习使用 数据清洗和特征工程Ch06 1 特征工程 1 1 特征工程介绍 1 2 特征预处理 1 2 1 数据清洗 1 2 1 1 数据清洗 预处理 1 2 1 2 数据清洗 格式内容错误数据清洗 1 2 1 3 数据清洗 逻辑错误
  • linux kernel development 3rd

    很早就听说这本书了 但是一直未看 现在稍微看看 第3章 1 vfork与fork不一样 但是也没多大作用 不看也罢 2 进程pcb task struct保存在process kernel栈的底端 而而thread info在栈顶端 不知具
  • C语言.表白神器.爱你之心之闪耀

    前言 爱你之心之闪耀 这个名字比较沙雕哈哈哈 爱你之心之闪耀 前言 爱心函数的选取 爱心函数1 爱心函数2 简单爱心 粒子发射原理 爱心结构 一些宏 初始化 init 创建若干爱心并初始化 setHeart 展示爱心 showHeart 爱
  • Vue3的使用

    1 安装命令步骤 vue create 命令 接下来我们创建 runoob vue3 app 项目 vue create runoob vue3 app 执行以上命令会出现安装选项界面 Vue CLI v4 4 6 Please pick
  • 解决升级JDK后:找不到sun.misc.Unsafe的类文件

    原因 JDK9以后已经将sun misc Unsafe弃用 同时改进了lib文件的存储方式 将sun misc Unsafe全部存储在了jdk unsupported里面 解决 手动添加弃用的类文件 在编译时 携带参数 add module
  • 运维Shell脚本小试牛刀(七):在函数文脚本件中调用另外一个脚本文件中函数

    运维Shell脚本小试牛刀 一 运维Shell脚本小试牛刀 二 运维Shell脚本小试牛刀 三 cd dirname 0 pwd 命令详解 运维Shell脚本小试牛刀 四 多层嵌套if elif elif else fi 蜗牛杨哥的博客 C
  • react 字段拼接

    Array from columns el index1 gt
  • 正确解决:坑爹的0xc000007b

    1 出现0xc000007b 应用程序无法正常启动 其根本原因是缺乏所需要的DLL 提供了错误版本的dll相当于没有DLL 提示完全一样 2 网上有人说缺乏dx运行环境 如果属实 也是因为缺乏dx相关的dll 所以根本原因 在低一点 3 与
  • PyCharm运行Python代码时出现“未找到模块”错误

    PyCharm运行Python代码时出现 未找到模块 错误 在PyCharm中执行Python脚本时 有时会出现 ModuleNotFoundError No module named XXXX 错误 这种错误是因为在项目中没有安装对应的
  • C++求vector中的最大值

    习惯了Python的编程以后 再回过头来写C 感觉头都被搞大了 Python是一门高级语言 而C 是一门偏底层的语言 所以Python一行解决的问题用C 也许需要好几行 比如在一个列表中找最大值的问题 如果是Python的话 那么代码大概是
  • (二十八)业绩归因之Brinson模型

    单期Brinson模型 一个时期的基金收益可以分为四个部分 资产配置收益 个股选择收益 交互收益和基准组合收益 先构建4个概念性的组合 Q1 Q4 基准收益组合和实际投资组合 Q2 积极资产配置组合 这表示基金经理能自主选择资产配置的比例
  • 定时器中断控制LED状态实验

    一 STM32 通用定时器简介 STM32F1 的通用定时器是一个通过可编程预分频器 PSC 驱动的 16 位自动装载计数 CNT 构成 STM32 的通用定时器可以被用于 测量输入信号的脉冲长度 输入捕获 或者产生输出波 输出比较和 PW
  • C++虚函数详解

    C 虚函数详解 前言 C 的特性使得我们可以使用函数继承的方法快速实现开发 而为了满足多态与泛型编程这一性质 C 允许用户使用虚函数 virtual function 来完成 运行时决议 这一操作 这与一般的 编译时决定 有着本质的区别 虚