C++ 虚函数和虚表

几篇写的不错的文章,本文是整合了这几篇文章,感谢这些大佬

https://www.jianshu.com/p/00dc0d939119

https://www.cnblogs.com/hushpa/p/5707475.html

https://www.jianshu.com/p/91227e99dfd7

多态:

多态是面相对象语言一个重要的特性,多态即让同一个用户自定义类型的对象在不同的决策时机呈现不同的行为实现
C++中的多态就分为

  • 编译时多态:就包括类成员函数重写operator函数重载
  • 运行时多态:C++编译器在运行时,根据决策逻辑判断传入所对象的类型,然后查找并根据该类虚表中的虚成员函数的地址,以进行动态调度目标类中的成员函数。

接下来就说下运行时多态的核心,虚函数和其背后的虚表。

虚函数

用virtual关键字修饰的函数就叫虚函数

因为vTable(虚表)是C++利用runtime来实现多态的工具,所以我们需要借助virtual关键字将函数代码地址存入vTable来躲开静态编译期。这里我们先不深入探究,后面我会细说。

首先我们先来看一个没有虚函数,即没有用到vTable的例子:

#include <iostream>
#include <ctime>
using std::cout;
using std::endl;struct Animal { void makeSound() { cout << "动物叫了" << endl; } };struct Cow : public Animal { void makeSound() { cout << "牛叫了" << endl; } };
struct Pig : public Animal { void makeSound() { cout << "猪叫了" << endl; } };
struct Donkey : public Animal { void makeSound() { cout << "驴叫了" << endl; } };int main(int argc, const char * argv[])
{srand((unsigned)time(0));int count = 4;while (count --) {Animal *animal = nullptr;switch (rand() % 3) {case 0:animal = new Cow;break;case 1:animal = new Pig;break;case 2:animal = new Donkey;break;}animal->makeSound();delete animal;}return 0;
}

程序中有一个基类Animal,它有一个makeSound()函数。有三个继承自Animal的子类,分别是牛、猪、驴,并且实现了自己的makeSound()方法。很简单的代码,是吧。

我们运行程序,你觉得输出结果会是什么呢?不错,这里会连续执行4次Animal的makeSound()方法,结果如下:

为什么?因为我们的基类Animal的makeSound()方法没有使用Virtual修饰,所以在静态编译时就makeSound()的实现就定死了。调用makeSound()方法时,编译器发现这是Animal指针,就会直接jump到makeSound()的代码段地址进行调用。

ok,那么我们把Animal的makeSound()改为虚函数,如下:

struct Animal { virtual void makeSound() { cout << "动物叫了" << endl; } };

运行会是怎样?如你所料,多态已经成功实现:

 

接下来就是大家最关心的部分,这是怎么回事?编译器到底做了什么?

虚表

为了说明方便,我们需要修改一下基类Animal的代码,不改变其他子类,修改如下:

struct Animal {virtual void makeSound() { cout << "动物叫了" << endl; }virtual void walk() {}void sleep() {}
};struct Cow : public Animal { void makeSound() { cout << "牛叫了" << endl; } };
struct Pig : public Animal { void makeSound() { cout << "猪叫了" << endl; } };
struct Donkey : public Animal { void makeSound() { cout << "驴叫了" << endl; } };

首先我们需要知道几个关键点:

  1. 函数只要有virtual,我们就需要把它添加进vTable。
  2. 每个类(而不是类实例)都有自己的虚表,因此vTable就变成了vTables。
  3. 虚表存放的位置一般存放在模块的常量段中,从始至终都只有一份。详情可在此参考

我们怎么理解?从本例来看,我们的Animal、Cow、Pig、Donkey类都有自己的虚表,并且虚表里都有两个地址指针指向makeSound()和walk()的函数地址。一个指针4个字节,因此每个vTable的大小都是8个字节。如图:

 

他们的虚表中记录着不同函数的地址值。可以看到Cow、Pig、Donkey重写了makeSound()函数但是没有重写walk()函数。因此在调用makeSound()时,就会直接jump到自己实现的code Address。而调用walk()时,则会jump到Animal父类walk的Code Address。

虚指针

现在我们已经知道虚表的数据结构了,那么我们在堆里实例化类对象时是怎么样调用到相应的函数的呢?这就要借助到虚指针了(vPointer)。

虚指针是类实例对象指向虚表的指针,存在于对象头部,大小为4个字节,比如我们的Donkey类的实例化对象数据结构就如下:

 

我们修改main函数里的代码,如下:

int main(int argc, const char * argv[])
{int count = 2;while (count --) {Animal *animal = new Donkey;animal->makeSound();delete animal;}return 0;
}

我们在堆中生成了两个Donkey实例,运行结果如下:

驴叫了
驴叫了
Program ended with exit code: 0

 

没问题。然后我们再来看看堆里的结构,就变成了这样:

 进一步探究虚表的内存布局

#include <iostream>
class Employee{
public:bool iService=true;virtual ~Employee(){};virtual void add_salary(){std::cout<<"add_salary method in Employee"<<std::endl;}
};class Teamer:public Employee{
public:int idNo=1000;virtual ~Teamer(){}void add_salary(){std::cout<<"add_salary method in Teamer"<<std::endl;}virtual void info(){std::cout<<"Teamer info for Teamer"<<std::endl;}void show(){std::cout<<"show method in Teamer"<<std::endl;}
};
int main(void){Employee *tm1=new Teamer();Employee *tm2=new Teamer();Employee *pp1=new Employee();Employee *pp2=new Employee();delete tm1,tm2,pp1,pp2;
}

在这里我们可以尝试打印*tm1,*tm2,*pp1和 *pp2,如下图所示

从上图的输出中,我们要引入一个虚指针(_vptr)的概念

  • 虚类的对象初始化时会自动创建一个隐藏的数据成员_vptr指针指向虚表,此前声明该虚类的对象编译器也创建了该虚类的虚表
  • 后续同一个虚类所有对象实例共享同一个虚表,截图中的tm1和tm2的隐藏指针指向同一个地址0x400cf0,pp1和pp2的虚表是同理如是.
  • 虚表表当前的地址是一个已经+16字节偏移后的内存地址

另外我们还打印出所有Teamer对象和Employee对象,他们获得内存分配都为16个字节。因此我们不妨在查看我们刚才实例化的所有对象。

查看对象的内存数据

现在我们不妨看看刚才实例化的各个对象的内存布局,使用x命令,因为每个对象的堆内存块尺寸都为16个字节,因此我们使用x/16xb将他们的内存数据转存到屏幕中,如下图所示。

  • _vptr在虚类的对象中就占用8个字节,该_vptr存储了指向该虚类的虚表的内存地址值。
  • iService是一个bool类型仅占用1个字节,另外高位的3个字节空间由于内存对齐的原因都以0填充。
  • idNo是一个4字节的int类型,对于Teamer的对象0x03e8的值就是十进制的1000,对于Employee的对象这里的4个字节由于按8字节内存对齐,仅作为填充位之用。

     

备注:这里我们回顾了内存对齐的相关知识。

探究虚表的内存布局

我们从前文打印的第一个Teamer对象 tm1的信息中,可以知道其_vptr指针指向0x400cf0,你是否发现“<虚表 for Teamer+16>”的字样。这个其实表明0x400cf0是已经+16字节偏移后的地址值

我们已经在前文提到在首个的新的虚类对象且初始化时,编译器会该类动态创建一个虚表,但为什么每个不同虚类的虚表都要额外偏移16个字节呢? 在本示例中,我们不妨减去这个偏移量,也即得到0x400ce0这个地址,然后使用x命令,该命令将300字节的内存数据转储到屏幕。

(gdb) x/300xb 0x400ce0

上面的命令以十六进制格式打印300字节,从0x400d00开始。 为什么要这个地址? 因为在上面我们看到类Teamer的虚表指针指向0x400d10,该地址已经偏移0x10个字节,即减去0x10就能得到原本虚表的地址。

下图中_ZTV是虚表的前缀,_ZTS是type-string(名称)的前缀,_ZTI是type-info的前缀。

我们从下图可以得到很多虚表的内存细节。

  • 每个Teamer虚表存在一个虚表表头占用16个字节,前8个字节0填充,后8个字节包含一个指向与该类对应的typeinfo表的地址(没必要理会,只需知道他们占用16个字节即可)。
  • 每个typeinfo表的前面也包含一个typeinfo name的信息(没必要理会,l罗列出来只是让你知道有这么一个描述字段)
  • 绿色的部分就是不同虚类的虚表,虚表就是包含了该类定义的所有virtual成员函数的函数地址。

     

我们可以从上图中绿色部分的内存数据中即每行冒号之后的8字节空间提取有用的数据,例如

  • 0x400cf0到0x400d08的内存区域中的内存数据,对应的是Teamer类类虚表中virtual成员函数地址的条目。
  • 0x400d30到0x400d40的内存区域中的内存数据,对应的是Employee类虚表中virtual成员函数地址的条目

我们这两个内存区域的数据分别整理成如下表,注意写本文时使用的是CentOS 7的x64小端机器,因此读取图中的内存数据时,是从右向左读取,因此整理下表每个内存位置对应的值,并且分别是有info symbol命令 再次查看每个内存位置的值对应的具体含义。

结合整理如下表可知:虚表中的地址值分别代表虚拟类中对应虚函数的地址

虚表内存布局

 

更简单获取虚类的虚表条目的另外一条命令就是info vtbl,这里就不展示了,我们看到上图的虚表中的虚解构函数都成对地出现,我们先暂不讨论为什么会这样,因为我日后会令起一文再阐述该问题。

  • 第一个解构函数,称为完整对象解构函数(complete object destructor),执行销毁操作时无需在对象上调用delete()。
  • 第二个解构函数称为删除析构函数( deleting destructor),在销毁对象后调用delete()。
  • 两者都摧毁了任何虚拟基类.一个独立的非虚函数称为基类对象解构函数(base object destructor)执行对象的销毁操作,但不执行其虚拟基类子对象的销毁操作,并且不调用delete()。
  • 非虚函数是静态绑定的(编译时绑定),因此在虚表中不存在任何非虚函数。

虚表构建细节

我们仍然使用上文的调用示例代码

int main(void){//Employee *tm1=new Teamer();Employee *tm2=new Teamer();Employee *pp1=new Employee();Employee *pp2=new Employee();delete tm1,tm2,pp1,pp2;
}

从上面的示例代码中我们已经知道

 

  • 首先,每个使用虚函数的类或从基类派生的虚函数的类都被赋予自己的虚表。该表只是C++编译器在“编译时”设置的静态数组。虚表包含当前类中所有虚成员函数的函数指针的相关条目,那么填入虚表的虚成员函数指针有四种来源。
    1. 派生类本身原创定义的虚函数,例如上图的Teamer::info()函数。

    2. 从父类继承的虚成员函数,且该函数未被派生类重写

    3. 从父类继承的虚成员函数,但该函数已被派生类重写。值的注意的是,虚表的虚成员函数指针始终指向该类中的最新的派生版本的虚成员函数。理解这句话非常重要!举个例子Teamer类从Employee类继承了add_salary()函数,但Teamer类重写(注意:不是重载)了该add_salary()函数,对于Teamer虚表来说,填入表中的add_salary()函数的地址是0x400b3e,而不是父类的add_salary()的地址0x400ab4。

    4. 若当前类定义了虚解构函数,那么该类的虚解构函数的解构函数的地址会“成双成对”地填入虚表中。按照惯例,由于定义类时优先定义解构函数,再实现其他成员函数,因此该虚解构函数对的地址通常会出现在表中头两行,上图是很好的例证。

  • 然后,当类对象实例化时会将*_vptr设置为指向该类的虚表。例如,当创建类型为Teamer的对象时*_vptr设置为指向Teamer的虚表。构造类型为Employee对象时,*_vptr设置为指向的Employee的虚表。我们这里先不讨论virtual解构函数,目前只针对其他虚函数进行讨论。
  • 对于基类Employee类型的对象,它只能访问Employee的成员,Employee类型的对象无法访问Teamer类的的成员函数,因为地址为0x400ab4的地址仅指向Employee::salary()
  • 同理,Teamer类型的对象也只能访问Teamer::add_salary()和Teamer::info()。

总结:

用一张图说明一切

 

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

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

相关文章

Unity 2017 Game Optimization 读书笔记(2)Scripting Strategies Part 2

1. Share calculation output 和上一个Tip很像&#xff0c;可以缓存计算结果或者各种信息&#xff0c;避免多次重复的计算&#xff0c;例如在场景里查找一个物体&#xff0c;从文件读取数据&#xff0c;解析Json等等。 容易忽略的点是常常在基类了实现了某个方法&#xff0c;在…

Unity 2017 Game Optimization 读书笔记(3)Scripting Strategies Part 3

1.Avoid retrieving string properties from GameObjects 通常来讲&#xff0c;从C#的object中获取string 属性没有额外的内存开销&#xff0c;但是从Unity中的Gameobject获取string属性不一样&#xff0c;这会产生上一篇讲到的 Native-Managed Bridge&#xff08;Native内存和…

Unity 2017 Game Optimization 读书笔记(4)Scripting Strategies Part 4

1.Avoid Find() and SendMessage() at runtime SendMessage() 方法和 GameObject.Find() 相关的一系列方法都是开销非常大的。SendMessage()函数调用的耗时大约是一个普通函数调用的2000倍&#xff0c;GameObject.Find() 则和场景的复杂度相关&#xff0c;场景越复杂&#xff0…

Unity HDRP中的光照烘焙测试(Mixed Lighing )和间接光

部分内容摘抄自&#xff1a;https://www.cnblogs.com/murongxiaopifu/p/8553367.html 直接光和间接光 大家都知道在Unity中&#xff0c;我们可以在场景中布置方向光、点光、聚光等类型的光源。但如果只有这些光&#xff0c;则场景内只会受到直接光的影响&#xff0c;而所谓的…

聊聊Unity项目管理的那些事:Git-flow和Unity

感谢原作者https://www.cnblogs.com/murongxiaopifu/p/6086849.html 0x00 前言 目前所在的团队实行敏捷开发已经有了一段时间了。敏捷开发中重要的一个话题便是如何对项目进行恰当的版本管理。项目从最初使用svn到之后的Git One Track策略再到现在的GitFlow策略&#xff0c;中…

聊聊网络游戏同步那点事

写的非常好的一篇博文&#xff0c;转载自https://www.cnblogs.com/murongxiaopifu/p/6376234.html 0x00 前言 16年年底的时候我从当时的公司离职&#xff0c;来到了目前任职的一家更专注于游戏开发的公司。接手的是一个platform游戏项目&#xff0c;基本情况是之前的团队完成…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics(1)

The Rendering Pipeline 渲染表现差有可能取决于CPU端&#xff08;CPU Bound&#xff09;也有可能取决于GPU(GPU Bound).调查CPU-bound的问题相对简单&#xff0c;因为CPU端的工作就是从硬盘或者内存中加载数据并且调用图形APU指令。想找到GPU-bound的原因会困难很多&#xff…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics(2)

Lighting and Shadowing 现代的游戏中&#xff0c;基本没有物体能在一步就完成渲染&#xff0c;这是因为有光照和阴影的关系。光照和阴影的渲染在Fragment Shader中需要额外的pass。 首先要设置场景中的Shadow Casters和Shadow Receivers&#xff0c;Shadow Casters投射阴影&…

Unity 2017 Game Optimization 读书笔记 The Benefits of Batching

batching&#xff08;合批&#xff09; 和大量的描述一个3D物体的数据有关系&#xff0c;比如meshes&#xff0c;verices&#xff0c;edges&#xff0c;UV coordinates 以及其他不同类型的数据。在Unity中谈论batching&#xff0c;指的是用于合批mesh数据的两个东西&#xff1a…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (3)

Rendering performance enhancements Enable/Disable GPU Skinning 开启GPU Skinning可以减轻CPU或GPU中Front End部分中某一个的负担&#xff0c;但是会加重另一个的负担。Skinning是mesh中的顶点根据动画中骨骼的当前位置进行计算&#xff0c;从而让角色摆出正确的姿势。 …

Unity手游开发札记——布料系统原理浅析和在Unity手游中的应用

原文&#xff1a;https://zhuanlan.zhihu.com/p/28644618 0. 前言 项目技术测试结束之后&#xff0c;各种美术效果提升的需求逐渐成为后续开发的重点&#xff0c;角色效果部分的提升目标之一便是在角色选择/展示界面为玩家提供更高的品质感&#xff0c;于是可以提供动态效果的…

行为树(Behavior Tree)实践(1)– 基本概念

原文&#xff1a;http://www.aisharing.com/archives/90 行为树&#xff08;Behavior Tree&#xff09;实践&#xff08;1&#xff09;– 基本概念 自从开博以来&#xff0c;每天都会关心一下博客的访问情况&#xff0c;看到一些朋友的订阅或者访问&#xff0c;不胜欣喜&…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (5) Shader优化

Shader optimization Fill Rate和 Memory Bandwidth开销最大的地方就是Fragment Shader。开销多大取决于Fragment Shader的复杂程度&#xff1a;多少纹理需要采样&#xff0c;多少数学计算函数需要使用等等。GPU的并行特性意味着在线程中如果任何地方存在瓶颈&#xff0c;都会…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (6)

1. Use less texture data 这条优化技巧非常直接&#xff0c;减少texture的数据量&#xff0c;减少分辨率或者降低位数&#xff0c;虽然可能会降低渲染质量。但是通常使用16-bit textures并不会明显的感觉到渲染效果下降。 MipMap技术可以有效减少VRAM和Texture Cache之间来回…

LeetCode 面试题57 - II(剑指offer) 和为s的连续正数序列

今天毕业五年了&#xff0c;一直忙于工作和享受&#xff0c;自从当年找完工作后就一直没有再刷过题&#xff0c;作为搬砖的码农&#xff0c;觉得还是应该养成长期刷题的习惯坚持下去。之前坚持了每天被一会单词&#xff0c;如今雅思一本也快看完了&#xff0c;从今天开始准备在…

反走样技术相关文章

https://zhuanlan.zhihu.com/p/28800047 https://zhuanlan.zhihu.com/p/57503957 https://zhuanlan.zhihu.com/p/33444125 https://zhuanlan.zhihu.com/p/33444429 走样的原因及其分类 说到走样&#xff0c;首先要说的就是采样。这也算是很多图形学专著中提到反走样相关技…

求n的阶乘的算法框图_单片机常用的14个C语言算法

问&#xff1a;怎么每天看到这种文章&#xff1f;答&#xff1a;只需搜索公众号"51单片机学习网"免费关注算法(Algorithm)&#xff1a;计算机解题的基本思想方法和步骤。算法的描述&#xff1a;是对要解决一个问题或要完成一项任务所采取的方法和步骤的描述&#xff…

LeetCode 286. 墙与门 多源BFS和DFS

思路1&#xff1a; DFS&#xff0c;对于每个门进行一次DFS搜索&#xff0c;记录每个位置对每个门的距离&#xff0c;当有更小距离的门时更新这个数值 public void WallsAndGates(int[][] rooms) {for (int i 0; i < rooms.GetLength(0); i){for (int j 0; j < rooms[i]…

贝塞尔曲线

文章参考于&#xff1a;https://www.jianshu.com/p/0c9b4b681724 https://gameinstitute.qq.com/community/detail/129188 贝赛尔曲线的前世今生&#xff1a; 贝塞尔曲线&#xff0c;这个命名规则一眼看上去大概是一个叫贝塞尔的数学家发明的。但&#xff0c;贝塞尔曲线依据…

如何控制局域网网速_单臂路由|N1盒子(OpenWRT)单线多拨实现网速叠加

受于某些现实条件&#xff0c;有些同学苦于家里的网络带宽太小无法尽情冲浪。如何在不额外花钱升级宽带的情况下提升家里网络呢&#xff1f;且看一个盒子加路由器即可实现网络单线多拨&#xff0c;成倍提升网速完成网速叠加。题图是我家里的路由器加N1盒子。前置条件光猫有超级…