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

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()。

总结:

用一张图说明一切

C++ 虚函数和虚表相关推荐

  1. C++_vptr与vtbl,虚函数与虚表

    C++_vptr与vtbl,虚函数与虚表 一:虚函数表指针(vptr)创建时机 vptr跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来,也就是运行的时候. 当程序在编译期间,编译器会 ...

  2. 虚函数、虚表的生成,虚表的修改

    接上文. 虚函数.虚表在没有实例的情况下是无法从语法层面进行访问的. 那么其到底有没有生成呢? #include<iostream> using namespace std;class A ...

  3. C++——多态|虚函数|重写|虚表

    文章目录 1. 多态的概念 1.1 概念 2. 多态的定义及实现 2.1多态的构成条件 2.2 虚函数 2.3虚函数的重写 虚函数重写的三个例外: 2.4 普通调用和多态调用: 2.5 C++11 o ...

  4. 虚函数,虚表深度剖析

    面向对象,从单一的类开始说起. class A { private:int m_a;int m_b; }; 这个类中有两个成员变量,都是int类型,所以这个类在内存中占用多大的内存空间呢? sizeo ...

  5. 虚函数和虚表指针的例子

    [2021年9月2日 add] C++中, 一旦某个成员函数在基类中声明为虚函数,则它在所有的子类中都会成为虚函数. 换言之,如果基类中已经声明了某个函数为虚函数,则无需在子类中使用关键字virtua ...

  6. C++中虚函数与多态实现

    多态,什么是多态?在计算机语言中,多态就是指一个接口或者方法,有多种展现形态.在C++中,通过父类指针调用子类方法,可以让父类指针有多种形态. C++中实现多态的方式有:虚函数,重载,模板,绑定等.此 ...

  7. C++类内存分布——深度理解继承与虚函数

    1.前言与准备 工欲善其事,必先利其器,我们先用好Visual Studio工具,像下面这样一步一步来:       先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 report ...

  8. java 析构函数_C++虚函数

    码字不易,欢迎给个赞! C++虚函数是多态性实现的重要方式,当某个虚函数通过指针或者引用调用时,编译器产生的代码直到运行时才能确定到底调用哪个版本的函数.被调用的函数是与绑定到指针或者引用上的对象的动 ...

  9. [C++] - 纯虚函数 抽象基类 接口类

    翻译自:https://www.learncpp.com/cpp-tutorial/126-pure-virtual-functions-abstract-base-classes-and-inter ...

最新文章

  1. python定义一个类和子类_Python定义类、定义子类以及super()函数的使用
  2. WHEN OVERSEAS
  3. k8s控制器:Job和CronJob
  4. 大小仅1MB,超轻量级通用人脸检测模型登上GitHub趋势榜
  5. pyecharts官方文档_中国疫情地图pyecharts
  6. Shell编程之条件语句(if语句,case分支语句)
  7. Three.js 新旧版本区别
  8. 查询SQLSERVER执行过的SQL记录(历史查询记录)
  9. Libvirt虚拟机的Qos与Cgroup
  10. 【汉化】DevExpress插件中RichEdit控件的自定义汉化方法
  11. 这些“大学”,将被除名!
  12. Java编写网络爬虫
  13. JavaScript 字符串函数
  14. ET游戏框架之环境搭建与运行
  15. 小学计算机教师面试试题及答案,2019下半年小学信息技术教师资格证面试真题及答案汇总...
  16. python爬虫-批量下载qq音乐
  17. Linux内存工具解析之RSS/VSS/USS/PSS区别于联系
  18. ggplot2学习笔记7:通过图层逐层构建图形
  19. (私人收藏)2019WER积木教育机器人赛(普及赛)解决方案-(全套)采集深度学习样本
  20. 同一绘制图区绘制多图

热门文章

  1. 查看ssh端口号_萌新云服务器折腾记-SSH配置
  2. Statues(三维bfs)
  3. 数模笔记_多变量最优化的拉格朗日乘子方法中的灵敏性分析和影子价格
  4. 宏内核linux,Linux 为什么还要坚持使用宏内核?
  5. python群发邮箱软件下载_用python群发电子邮件
  6. 【系统架构设计师】2020-08-05
  7. mycat连接mysql时间_Mycat连接MySQL 8时的注意事项
  8. android 4个布局,Android - 4种基本布局
  9. ik分词器实现原理_SpringBoot整合Elasticsearch实现商品搜索
  10. Zookeeper集群搭建(配置详解)