1.基础理论

为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”

虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。

其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针

因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。

2.实现与内部结构¶
下面我们来看自动与手动操纵vptr来获取地址与调用虚函数!

开始看代码之前,为了方便大家理解,这里给出调用图:

/*** @file vptr1.cpp* @brief C++虚函数vptr和vtable* 编译:g++ -g -o vptr vptr1.cpp -std=c++11* @author 光城* @version v1* @date 2019-07-20*/#include <iostream>
#include <stdio.h>
using namespace std;/*** @brief 函数指针*/
typedef void (*Fun)();/*** @brief 基类*/
class Base
{public:Base(){};virtual void fun1(){cout << "Base::fun1()" << endl;}virtual void fun2(){cout << "Base::fun2()" << endl;}virtual void fun3(){}~Base(){};
};/*** @brief 派生类*/
class Derived: public Base
{public:Derived(){};void fun1(){cout << "Derived::fun1()" << endl;}void fun2(){cout << "DerivedClass::fun2()" << endl;}~Derived(){};
};
/*** @brief 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚表** @param obj* @param offset** @return*/
Fun getAddr(void* obj,unsigned int offset)
{cout<<"======================="<<endl;void* vptr_addr = (void *)*(unsigned long *)obj;  //64位操作系统,占8字节,通过*(unsigned long *)obj取出前8字节,即vptr指针printf("vptr_addr:%p\n",vptr_addr);/*** @brief 通过vptr指针访问virtual table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*(unsigned long *)vptr_addr取出前8字节,* 后面加上偏移量就是每个函数的地址!*/void* func_addr = (void *)*((unsigned long *)vptr_addr+offset);printf("func_addr:%p\n",func_addr);return (Fun)func_addr;
}
int main(void)
{Base ptr;Derived d;Base *pt = new Derived(); // 基类指针指向派生类实例Base &pp = ptr; // 基类引用指向基类实例Base &p = d; // 基类引用指向派生类实例cout<<"基类对象直接调用"<<endl;ptr.fun1();cout<<"基类对象调用基类实例"<<endl;pp.fun1();cout<<"基类指针指向派生类实例并调用虚函数"<<endl;pt->fun1();cout<<"基类引用指向派生类实例并调用虚函数"<<endl;p.fun1();// 手动查找vptr 和 vtableFun f1 = getAddr(pt, 0);(*f1)();Fun f2 = getAddr(pt, 1);(*f2)();delete pt;return 0;
}
基类对象直接调用
Base::fun1()
基类对象调用基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:0x5555f5d20cf0
func_addr:0x5555f5b2005c
Derived::fun1()
=======================
vptr_addr:0x5555f5d20cf0
func_addr:0x5555f5b20094
DerivedClass::fun2()
我们发现C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。例如,上述通过基类指针指向派生类实例,并调用虚函数,将上述代码简化为:
Base *pt = new Derived(); // 基类指针指向派生类实例
cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
pt->fun1();

其过程为:首先程序识别出fun1()是个虚函数,其次程序使用pt->vptr来获取Derived的虚拟表。第三,它查找Derived虚拟表中调用哪个版本的fun1()。这里就可以发现调用的是Derived::fun1()。因此pt->fun1()被解析为Derived::fun1()!

除此之外,上述代码大家会看到,也包含了手动获取vptr地址,并调用vtable中的函数,那么我们一起来验证一下上述的地址与真正在自动调用vtable中的虚函数,比如上述pt->fun1()的时候,是否一致!

这里采用gdb调试,在编译的时候记得加上-g。

通过gdb vptr进入gdb调试页面,然后输入b Derived::fun1对fun1打断点,然后通过输入r运行程序到断点处,此时我们需要查看调用栈中的内存地址,通过disassemable fun1可以查看当前有关fun1中的相关汇编代码,我们看到了0x0000000000400ea8,然后再对比上述的结果会发现与手动调用的fun1一致,fun2类似,以此证明代码正确!

gdb调试信息如下:

(gdb) b Derived::fun1
Breakpoint 1 at 0x400eb4: file vptr1.cpp, line 23.
(gdb) r
Starting program: /home/light/Program/CPlusPlusThings/virtual/pure_virtualAndabstract_class/vptr
基类对象直接调用
Base::fun1()
基类引用指向派生类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数Breakpoint 1, Derived::fun1 (this=0x614c20) at vptr1.cpp:23
23              cout << "Derived::fun1()" << endl;
(gdb) disassemble fun1
Dump of assembler code for function Derived::fun1():0x0000000000400ea8 <+0>: push   %rbp0x0000000000400ea9 <+1>: mov    %rsp,%rbp0x0000000000400eac <+4>: sub    $0x10,%rsp0x0000000000400eb0 <+8>: mov    %rdi,-0x8(%rbp)
=> 0x0000000000400eb4 <+12>:    mov    $0x401013,%esi0x0000000000400eb9 <+17>:    mov    $0x602100,%edi0x0000000000400ebe <+22>:    callq  0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>0x0000000000400ec3 <+27>:    mov    $0x400a00,%esi0x0000000000400ec8 <+32>:    mov    %rax,%rdi0x0000000000400ecb <+35>:    callq  0x4009f0 <_ZNSolsEPFRSoS_E@plt>0x0000000000400ed0 <+40>:    nop0x0000000000400ed1 <+41>:    leaveq 0x0000000000400ed2 <+42>:    retq
End of assembler dump.
(gdb) disassemble fun2
Dump of assembler code for function Derived::fun2():0x0000000000400ed4 <+0>: push   %rbp0x0000000000400ed5 <+1>: mov    %rsp,%rbp0x0000000000400ed8 <+4>: sub    $0x10,%rsp0x0000000000400edc <+8>: mov    %rdi,-0x8(%rbp)0x0000000000400ee0 <+12>:    mov    $0x401023,%esi0x0000000000400ee5 <+17>:    mov    $0x602100,%edi0x0000000000400eea <+22>:    callq  0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>0x0000000000400eef <+27>:    mov    $0x400a00,%esi0x0000000000400ef4 <+32>:    mov    %rax,%rdi0x0000000000400ef7 <+35>:    callq  0x4009f0 <_ZNSolsEPFRSoS_E@plt>0x0000000000400efc <+40>:    nop0x0000000000400efd <+41>:    leaveq 0x0000000000400efe <+42>:    retq
End of assembler dump.

深入浅出C++虚函数的vptr与vtable相关推荐

  1. 深入浅出之虚函数原理篇(笔记三)

    上一节,我们讲到了虚函数,那么你知道虚函数是如何做到多态的吗? 虚函数是通过后期绑定,在执行时间接通过一张虚函数表,间接调用欲绑定的函数.表中的每一个元素都指向虚函数的地址.当然,编译器也会为类增加一 ...

  2. 虚函数指针(vptr)与虚函数表(vptb)

    1. 基类指针指向派生类对象 #include "stdafx.h" #include <iostream> using namespace std;class A { ...

  3. 构造函数为什么不能是虚函数 ( 转载自C/C++程序员之家)

    从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的.问题出来了,如果构造函数是虚的,就需要通过 vtable来调用, ...

  4. 构造函数为什么不能是虚函数

    从存储空间角度看 虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的. 问题出来了,假设构造函数是虚的.就须要通过 vtable来 ...

  5. C++学习12:C++多态、虚函数、虚析构函数、纯虚函数、抽象类

    一 多态概述 C++中的多态分为静态多态和动态多态.静态多态是函数重载,在编译阶段就能确定调用哪个函数.动态多态是由继承产生的,指同一个属性或行为在基类及其各派生类中具有不同的语义,不同的对象根据所接 ...

  6. C++多态中虚函数的深入理解

    c++中动态多态性是通过虚函数来实现的.静态多态性是通过函数的重载来实现的,在程序运行前的一种早绑定,动态多态性则是程序运行过程中的一种后绑定.根据下面的例子进行说明. #include <io ...

  7. 关于虚函数的应用(10个例子)

    虚函数是C++中非常重要的一个概念,它最大的好处是能够触发动态绑定.C++中的函数默认不使用动态绑定,要触发动态绑定,必须满足 两个条件: 第一,只有指定为虚函数的成员函数才能进行动态绑定,成员函数默 ...

  8. C++中的虚函数与纯虚函数

    文章目录 1 C++中的虚函数 1.1 虚函数 1.2 单个类的虚函数表 1.3 使用继承的虚函数表 1.4 多重继承的虚函数表 2 C++中的纯虚函数 1 C++中的虚函数 1.1 虚函数 虚函数的 ...

  9. 虚函数、虚函数表、虚继承

    1.虚函数 虚函数的定义: 虚函数必须是类的 非静态成员函数(且非构造函数),其访问权限是public(可以定义为privateor proteceted, 但是对于多态来说,没有意义),在基类的类定 ...

最新文章

  1. 【转载】linux静态链接库与动态链接库的区别及动态库的创建
  2. 《数据科学家养成手册》第五章---矛盾的世界笔记
  3. [转载] 中华典故故事(孙刚)——08 狗咬吕洞宾
  4. 【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( 使用 DexClassLoader 获取组件类失败 | 失败原因分析 | 自定义类加载器没有加载组件类的权限 )
  5. springboot整合spring Cache(redis)
  6. malloc,free,new,delete解析(原)
  7. Jq获取同一名称单选框(radio)被选中的值
  8. 浏览器滚动条样式更改
  9. 现在mfc的现状如何_天玑云客:微信代运营现在什么现状?如何挑选合适的代运营公司?...
  10. js 弹出层的点击事件影响到底层的点击事件_聊一聊 Node.js 错误处理
  11. hihocoder216周:贪心或二分
  12. poj 3735 Training little cats (矩阵快速幂)
  13. springboot整合富文本编辑器
  14. 烽火HG680-MC_TTL免费升级固件及教程
  15. 免安装版mysql使用_免安装版MySql使用
  16. DKN: Deep Knowledge-Aware Network for News Recommendation
  17. 如何批量生成JAN13条码
  18. 51单片机-4G模块
  19. 动态规划之买瓜子—C说算法系列
  20. 表格样式的层叠顺序与优先级

热门文章

  1. 透视大数据时代下的物联网生活
  2. Android-使用FindBugs
  3. SD卡中FAT32文件格式快速入门(图文详细介绍)
  4. CentOS 6.5源码包安装MySQL
  5. Linux command: dos2unix
  6. 《编写高质量代码:改善C#程序的157个建议》勘误表
  7. 存储基础知识一 主要技术DAS、SAN、NAS
  8. 王式安概率论与数理统计基础课手写笔记-第一章概率与事件-第二章随机变量及其分布
  9. 以下哪个不是迭代算法的缺点_海量数据分库分表方案(一)算法方案
  10. mysql 慢查询 定位过程,和order by有关