【C++】浅析C++中的虚函数

news2024/9/20 16:43:36

关于虚函数

Q1:观察一个类引入虚函数后,类会发生什么变化?

首先,创建一个空类A,并实例化出A的一个对象a,计算一下这个对象占用多少字节:

#include<iostream>
using namespace std;

class A
{
	
};

int main(){
	A a;
	cout<<sizeof(a)<<endl;
	return 0;
}

输出结果:

1            //这里不要以为a占用的字节是0。声明出一个对象就会占用内存空间,哪怕是个空类也至少是1

接下来,我们在A中加入两个普通成员函数,再次观察:

#include<iostream>
using namespace std;

class A
{
	void fun1(){}
	void fun2(){}
};

int main(){
	A a;
	cout<<sizeof(a)<<endl;
	return 0;
}

输入结果:

1            //并没有变化,说明普通的类成员函数并不占用类对象的内存空间

再次向类A中添加一个虚函数,观察现象:

#include<iostream>
using namespace std;

class A
{
	void fun1(){}
	void fun2(){}
	virtual void vfun(){}
};

int main(){
	A a;
	cout<<sizeof(a)<<endl;
	return 0;
}

输出结果:

8           //这里的数值跟你的机器有关,我的是64位机器,也可能这个数在你的机器上是4,没关系道理是一样的

看到这里,我们至少可以得出结论:这个虚函数的增加引起了从1变成8的这种变化。

当一个或者多个虚函数被加入到类中之后,编译器会向类中插入一个看不见的成员变量。在类中,这个看不见的成员变量类似是下面这样的伪代码:

class A
{
	void *vptr;
	...
};

这个看不见的成员变量有一个名字叫做虚函数表指针(virtual table pointer,vptr)。这个指针的大小正好是8个字节,因此sizeof(a)的结果就变成了8。(指针占用多少字节取决于机器,一般情况下,32位机器上指针占用4个字节,64位机器上指针占用8个字节)

Q2: 虚函数表的生成时机和生成原因

由上一节的实验,我们得出了这样的结论:当一个类中的虚函数数量大于等于1时(换言之,当类中至少含有一个虚函数时),在编译期间,编译器就会为该类生成一个虚函数表(virtual table,vtbl),这个虚函数表会一直伴随着类.经过编译链接生成一个可执行文件后,这个类以及伴随着类的虚函数表都会保存在可执行文件中。当这个可执行文件执行的时候会一并载入内存。

Q3:虚函数表指针被赋值的时机

我们现在已经知道了虚函数表指针(vptr)和虚函数表(vtbl),那么这两个有什么关系呢?

对于上述有虚函数的类A,在编译期间,编译器会在类A的构造函数中,安插为vptr赋值的语句。

A(){
    vptr = &A::vftable;//编译器在编译期间做的
    ...
}

当创建类A对象的时候会执行类A的构造函数,因为构造函数中,有给vptr赋值的语句,从而就能够使vptr指向类A的vtbl;

当然,如果程序员没有书写自己的关于类A的构造函数的时候,编译器会自动为类A生成一个构造函数,并会自动安插给vptr赋值的语句。

这也就解释了为什么之前说构造函数不能为虚函数的原因:执行构造函数前对象尚未完成创建,虚函数表还不存在。

Q4:类对象在内存中的布局

我们为上述类A做一下小小的改变,添加一个虚函数fun2和一个虚析构函数,然后再添加两个成员变量:

class A
{
	public:
		void fun1(){}
		void fun2(){}
		virtual void vfun1(){}
		virtual void vfun2(){}
		virtual ~A(){}
	private:
		int m_a;
		int m_b;
};

这是我们再实例化一个类A的对象a:

A a;

这里可以看到在类A的内存中实际上有三个占用内存的块,除了成员变量之外,还有这个虚函数表指针vptr,并且,指向虚函数表的指针总是存在于对象实例中最前面的位置,这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下。

请添加图片描述
我们刚才聊到了vptr与vtbl的关系,毫无疑问,编译器会将这个vptr指向类A的虚函数表vtbl:
请添加图片描述
这个虚函数表中包含三个指针,这三个指针分别指向三个虚函数:
请添加图片描述
然后再加上两个普通成员函数:
请添加图片描述
虚函数表以及成员函数,这些统统属于类A的组成部分,但并不占用类A对象的内存空间,也就是sizeof这个对象的时候,得到的值应该只是一个虚函数表指针的地址加上两个成员变量的指针:

#include<iostream>
using namespace std;

class A
{
	public:
		void fun1(){}
		void fun2(){}
		virtual void vfun1(){}
		virtual void vfun2(){}
		virtual ~A(){}
	private:
		int m_a;
		int m_b;
};

int main(){
	A a;
	int* aa;
	cout<<sizeof(a)<<endl;
	return 0;
}

输出结果:

16

Q5:虚函数的工作原理以及多态性的体现

C++中有两种方式实现多态,即重载和覆盖。

  • 重载:是指允许存在多个同名函数,而这些函数的参数表不同(参数个数不同、参数类型不同或者两者都不同)。这种形式实现的多态为静态多态。
  • 覆盖:是指子类重新定义父类虚函数的做法,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针拥有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法,比如:模板元编程是在编译期完成的泛型技术,RTTI、虚函数则是在运行时完成的泛型技术。通过虚函数实现的多态称之为动态多态。

也就是说,动态多态必须存在虚函数,没有虚函数,绝对不可能存在动态多态。

代码实现

首先,从代码实现上:

可以看一下调用路线:是不是利用vptr找到vtbl,通过查询vtbl来找到虚函数表的入口地址,并去执行这个虚函数,如果调用这个虚函数的路线是走的这个路线,那么就是多态;如果调用这个虚函数走的不是这个路线,而是像调用普通成员函数一样直接调用,就不是多态。从这个角度来讲,就不用管什么继承关系,有没有什么子类。

例:

#include<iostream>
using namespace std;

class Base
{
	public:
		virtual void myvirfunc(){}
};

int main(){
    Base * pa = new Base();
    pa->myvirfunc();
    
    Base base;
    base.myvirfunc();
    
    Base* ybase = &base;
    ybase->myvirfunc();
	return 0;
}

利用这个方式去判定,得出中间的调用不是多态,上面和下面的都是多态。这一点其实通过汇编代码能很清晰的看出调用方式的差异。

(这里推荐一个看汇编代码的网站:Compiler Explorer (godbolt.org))
在这里插入图片描述

表现形式

其次,从表现形式上:

  • 程序中即要存在父类,也要存在子类,父类中必须包含虚函数,子类中也必须重写父类中的虚函数。
  • 父类指针要指向子类对象或者父类引用绑定子类对象。
  • 当通过父类的指针或引用,调用子类中重写的虚函数时,就能看出多态性的表现了。

也就是说,最后发现调用的是子类的虚函数。

class Derive : public Base
{
    public:
    vvirtual void myvirfunc(){}
}
//用父类指针指向子类对象
Derive derive;
Base * pbase = &derive;
pbase->myvirfunc();//Derive::myvirfunc()    
//或者
Base * pbase2 = new Derive();
pbase2->myvirfunc();//Derive::myvirfunc()

//父类引用指向子类对象
Derive derive2;
Base& yinbase = deriver2;
yinbase.myvirfunc();//Derive::myvirfunc()

这种有继承关系的内存布局:(假设基类Base有三个虚函数f,g,h,子类Derive重写了其中的g函数)
请添加图片描述
能够看到,由于子类Derive类重写了g函数,在它的虚函数表指针vtbl里,就指向了它自己的g函数,其他两个函数指向父类的f函数与h函数。

总结

虚函数(Virtual Function)是通过虚函数表(Virtual Table,简称为V-Table)来实现的。虚函数表主要存储的是指向一个类的虚函数地址的指针,通过使用虚函数表,继承、覆盖的问题都都得到了解决。假如一个类有虚函数,当我们构建这个类的实例时,将会额外分配一个指向该类虚函数表的指针,当我们用父类的指针来操作一个子类的时候,这个指向虚函数表的指针就派上用场了,它指明了此时应该使用哪个虚函数表,而虚函数表本身就像一个地图一样,为编译器指明了实际所应该调用的函数。指向虚函数表的指针是存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下),这就意味着理论上我们可以通过对象实例的地址得到这张虚函数表(实际上确实可以做到),然后对虚函数表进行遍历,并调用其中的函数。

#include <iostream>
#include <string>

typedef void (*Fun)(void);

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

    virtual void g()
    {
        std::cout << "Base::g()" << std::endl;
    }

    virtual void h()
    {
        std::cout << "Base::h()" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    Base base;
    Fun  fun1 = nullptr;
    Fun  fun2 = nullptr;
    Fun  fun3 = nullptr;

    std::cout << "指向虚函数表指针的地址:" << (long*)(&base) << std::endl;//输出类Base中虚函数表指针的地址(这个指针位于类内存中最前面,所以类的地址即为该指针的地址)
    std::cout << "虚函数表的地址:" << (long*)*(long*)(&base) << std::endl;//(这个指针是(long*)(&base),这个指针指向虚函数表,所以虚函数表是*(long*)(&base),因此虚函数表的地址是(long*)*(long*)(&base))

    std::cout << "offset_to_top: " << *((long*)*(long*)(&base) - 2) << std::endl;
    std::cout << "typeinfo for Base: " << (long*)*((long*)*(long*)(&base) - 1) << std::endl;

    fun1 = (Fun) * ((long*)*(long*)(&base));//前面推理出这个虚函数表地址是(long*)*(long*)(&base),表里第一个虚函数的地址是*((long*)*(long*)(&base))
    std::cout << "虚函数表中第一个函数的地址:" << (long*)fun1 << std::endl;
    fun1();

    fun2 = (Fun) * (((long*)*(long*)(&base))+1);
    std::cout << "虚函数表中第二个函数的地址:" << (long*)fun2 << std::endl;
    fun2();

    fun3 = (Fun) * ((long*)*(long*)(&base) + 2);
    std::cout << "虚函数表中第三个函数的地址:" << (long*)fun3 << std::endl;
    fun3();
}

程序运行结果:

指向虚函数表指针的地址:0x7ffca8cae9a8
虚函数表的地址:0x55cc55f1ad50
offset_to_top: 0
typeinfo for Base: 0x55cc55f1ad68
虚函数表中第一个函数的地址:0x55cc55f184c0
Base::f()
虚函数表中第二个函数的地址:0x55cc55f184fc
Base::g()
虚函数表中第三个函数的地址:0x55cc55f18538
Base::h()

本文仅对C++中的虚函数及其内存模型进行简要分析,如果有兴趣深入了解,请查看这篇文章:
一文读懂C++虚函数的内存模型

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

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

相关文章

02-阴影

使用阴影 1.给立方体添加castShadow&#xff0c;让立方体产生阴影 cube.castShadow true;2.创建一个地面用于接收阴影 const planeGemetry new THREE.PlaneGeometry(20, 30)const planeMaterial new THREE.MeshPhongMaterial({ color: 0xffffff })const plane new THREE.…

LeetCode 2. 两数相加

文章目录 1. 题目描述2. 解题代码 1. 题目描述 链接&#xff1a;https://leetcode.cn/problems/add-two-numbers/ 2. 解题代码 public ListNode AddTwoNumber(ListNode l1, ListNode l2) {ListNode head new ListNode();ListNode cur head;int carry 0;while (l1 ! null…

CIO 访谈|财达证券 IT 基础架构云化转型思考与实践

作为河北省证券行业的主力军&#xff0c;财达证券始终坚持用科技赋能业务&#xff0c;全方位推动信息化和数字化建设。在本期视频中&#xff0c;我们请到了财达证券首席信息官谢井民&#xff0c;分享如何基于 SmartX 超融合逐步实现 IT 基础架构云化转型&#xff0c;满足公司“…

RPC框架(一):扫盲

文章目录 一、概要二、RPC组成部分三、影响RPC框架性能的因素 一、概要 RPC作用&#xff1f; 让不同服务间调用方法像同一服务间调用本地方法一样 二、RPC组成部分 Client&#xff1a;RPC协议调用方 Server&#xff1a;远程服务方法的具体实现 Stub/Proxy&#xff1a;RPC代…

基于SpringBoot的家庭理财记账系统的设计与开发

1.引言 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用java语言技术和mysql数据库来完成对系统的设计。整个…

Apache组件POI,将图片下载到Excel文件中并导出。

在日常的工作中&#xff0c;有时我们会遇到需要将数据库表中图片字段下载到Excel中的需求&#xff0c;为方便各位小伙伴今后的开发工作&#xff0c;我将分享出自己写的代码&#xff0c;在文章末尾有我上传到 Gitee 上的 Demo案例&#xff0c;希望大家喜欢。 将图片下载到Excel文…

[CKA]考试之Deployment管理pod扩缩容

由于最新的CKA考试改版&#xff0c;不允许存储书签&#xff0c;本博客致力怎么一步步从官网把答案找到&#xff0c;如何修改把题做对&#xff0c;下面开始我们的 CKA之旅 题目为&#xff1a; Task 扩容 deployment guestbook 为 6个pod 注意&#xff0c;如果题目要求先切换K8…

低价高品质的头戴式降噪耳机,还支持主动降噪,QCY H4体验

每天办公的时候&#xff0c;我都喜欢戴上耳机听音乐&#xff0c;开会的时候也会方便一些。以前我用过无线入耳式耳机&#xff0c;但是戴时间长了会让耳朵很痛苦&#xff0c;因为室内也不算热&#xff0c;所以我觉得头戴式蓝牙耳机很合适&#xff0c;目前我用的是这款QCY H4头戴…

Laravel 多字段去重count计数

Laravel 多字段去重count计数 背景&#xff1a;需要统计数据列表总条数&#xff08;字段1、字段2去重统计&#xff09; table&#xff1a;policy_view,去重字段admin_id和permission 期望结果&#xff1a;count不含重复统计数据 解决思路&#xff1a; 语法&#xff1a;DISTI…

appium下载安装及环境配置及夜神模拟器下载、nodejs安装

appium所需要的环境有&#xff1a;jdk、android sdk、Nodejs、appium客户端 此次教程只针对于windows jdk、Android sdk已下载安装配置好环境&#xff0c;此次教程不再重复记录 jdk可看这篇博文jdk安装及环境配置 Android sdk 可看这篇 Android SDK下载与安装 一、Nodejs安装 …

有哪些做任务赚佣金的平台 做任务挣钱的app

科思创业汇 大家好&#xff0c;这里是科思创业汇&#xff0c;一个轻资产创业孵化平台。赚钱的方式有很多种&#xff0c;我希望在科思创业汇能够给你带来最快乐的那一种&#xff01; 做任务赚佣金的平台&#xff1f;做任务赚钱一直是一种流行的赚钱方式。现在有无数的app可以通…

优化细节令人惊叹,用户体验直接拉满!你的App也能如此丝滑!

启动时间是App使用者的第一体验&#xff0c;很多大厂都通过A/B实验论证启动速度的优化可以带来用户留存的显著收益&#xff0c;尤其是体量大的用户&#xff0c;启动时间缩短一点&#xff0c;留存增长一点&#xff0c;那就带来了非常大的收益。因此&#xff0c;启动性能优化一直…

济南中医风湿病医院受邀参加北坦街道“学习二十大 奋进新征程”主题党日活动

为学习贯彻党的二十大精神&#xff0c;中共天桥区北坦街道工作委员会于近日特组织开展“学习贯彻二十大精神 凝心聚力奋进新征程”联合主题党日活动&#xff0c;济南中医风湿病医院党支部预备党员卢雪梅同志受邀参加。 活动第一站&#xff0c;参观济南黄河文化展览馆。这是一部…

Microsoft Edge插件推荐:CSDN·浏览器助手

文章目录 1.简介2.安装3.总结 今天来给大家分享一个超级好用的Microsoft Edge插件&#xff0c;名为CSDN浏览器助手 1.简介 CSDN浏览器助手是一款集成本地书签、历史记录与 CSDN搜索(so.csdn.net) 的搜索工具&#xff0c;可以自定义Microsoft Edge的新标签页&#xff0c;还可以…

LangChain-Evaluation—如何评估LLM及其应用(三)

省流&#xff1a;目前没有真正完美的解决方案&#xff0c;比如分类有精度这样接近完美的评估方案&#xff0c;但LLM目前没有 This section of documentation covers how we approach and think about evaluation in LangChain. Both evaluation of internal chains/agents, b…

十五周算法训练营——背包问题

今天是十五周算法训练营的第十三周&#xff0c;主要讲背包问题专题。&#xff08;欢迎加入十五周算法训练营&#xff0c;与小伙伴一起卷算法&#xff09; 「背包问题&#xff1a;给你一个可装载重量为W的背包和N个物品&#xff0c;每个物品有重量和价值两个属性。其中第i个物品…

以加快工程和科学发展为使命,MathWorks 创新步履不止

一直以来&#xff0c;人类对于宇宙苍穹、高山峻岭的探索永不止步。今年是人类首次登月54周年&#xff0c;毋庸置疑&#xff0c;“登月计划”激发了全世界所有人的想象力。正由于这项计划&#xff0c;让耐高温的金属和合金防火面料、冻干食品及光伏电池、集成电路、计算机以及备…

解密Linux内核神器:内存屏障的秘密功效与应用方法

一、内存屏障简介 现在大多数现代计算机为了提高性能而采取乱序执行&#xff0c;这可能会导致程序运行不符合我们预期&#xff0c;内存屏障就是一类同步屏障指令&#xff0c;是CPU或者编译器在对内存随机访问的操作中的一个同步点&#xff0c;只有在此点之前的所有读写操作都执…

信号链噪声分析14

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 提示&#xff1a;这里可以添加技术概要 虽然噪声的均方根值可通过 q/√12 计算精确近似值&#xff0c;但在某些条件下&#xff0c;频域成分可能 与交流输入信号高度相关。例如&#xff0c;低幅度周期性信号的相关度大…

智慧厕所系统:革新公厕管理的智能解决方案

公厕是城市基础设施的重要组成部分&#xff0c;但由于管理难度大、人员分散等问题&#xff0c;公厕的管理一直是一个难题。智慧公厕系统通过智能化的监控设备和传感器&#xff0c;实时监测公厕的使用情况和卫生状况&#xff0c;并将数据传输到中央控制系统。管理员可以通过该系…