目录

  • 深入理解C++ 虚函数表

    • 虚函数表概述
    • 单继承下的虚函数表
      • 派生类未覆盖基类虚函数
      • 派生类覆盖基类虚函数
    • 多继承下的虚函数表
      • 无虚函数覆盖
      • 派生类覆盖基类虚函数
    • 钻石型虚继承
    • 总结
      • 几个原则
      • 安全性问题

深入理解C++ 虚函数表

​ C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

​ 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

本文将详细介绍虚函数表的实现及其内存布局。

虚函数表概述

虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。

C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置,这样通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

按照上面的说法,来看一个实际的例子:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};int main()
{Base t;(     ((void(*)())*((int*)(*((int*)&t)) + 0))   )     ();(     ((void(*)())*((int*)(*((int*)&t)) + 1))   )     ();(     ((void(*)())*((int*)(*((int*)&t)) + 2))   )     ();return 0;
}

经过VS2017,x86测试:

我们成功地通过实例对象的地址,得到了对象所有的类函数。

main定义Base类对象t,把&b转成int *,取得虚函数表的地址vtptr就是:(int*)(&t),然后再解引用并强转成int *得到第一个虚函数的地址,也就是Base::f()即(int*)(*((int*)&t)),那么,第二个虚函数g()的地址就是(int*)(*((int*)&t)) + 1,依次类推。

单继承下的虚函数表

派生类未覆盖基类虚函数

下面我们来看下派生类没有覆盖基类虚函数的情况,其中Base类延用上一节的定义。从图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址。

可以看到下面几点:

1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面

测试代码:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};class Devired :public Base{
public:virtual void x() { cout << "x()" << endl; }
};int main()
{Devired t;(((void(*)())   *((int*)(*((int*)&t)))))   ();(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();return 0;
}

测试效果:

派生类覆盖基类虚函数

再来看一下派生类覆盖了基类的虚函数的情形,可见:

  1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 (显然的,不然虚函数失去意义)
  2. 派生类没有覆盖的虚函数延用基类的

测试代码:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};class Derive :public Base{
public:virtual void x() { cout << "x()" << endl; }virtual void f() { cout << "Derive::f()" << endl; }
};int main()
{Derive t;(((void(*)())   *((int*)(*((int*)&t)))))   ();(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();return 0;
}

测试效果:

多继承下的虚函数表

无虚函数覆盖

如果是多重继承的话,问题就变得稍微复杂一丢丢,主要有几点:

  1. 每个基类都有自己的虚函数表
  2. 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后(这点和单继承无虚函数覆盖相同),具体见下图所示:

测试代码

#include <iostream>
class Base
{
public:Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }private:int m_iMem1;int m_iMem2;
};class Base2
{
public:Base2(int mem = 3) : m_iBase2Mem(mem) { ; }virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }private:int m_iBase2Mem;
};class Base3
{
public:Base3(int mem = 4) : m_iBase3Mem(mem) { ; }virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }private:int m_iBase3Mem;
};class Devired : public Base, public Base2, public Base3
{
public:Devired(int mem = 7) : m_iMem1(mem) { ; }virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }private:int m_iMem1;
};int main()
{// Test_3Devired d;int *dAddress = (int*)&d;typedef void(*FUNC)();/* 1. 获取对象的内存布局信息 */// 虚表地址一int *vtptr1 = (int*)*(dAddress + 0);int basemem1 = (int)*(dAddress + 1);int basemem2 = (int)*(dAddress + 2);int *vtpttr2 = (int*)*(dAddress + 3);int base2mem = (int)*(dAddress + 4);int *vtptr3 = (int*)*(dAddress + 5);int base3mem = (int)*(dAddress + 6);/* 2. 输出对象的内存布局信息 */int *pBaseFunc1 = (int *)*(vtptr1 + 0);int *pBaseFunc2 = (int *)*(vtptr1 + 1);int *pBaseFunc3 = (int *)*(vtptr1 + 2);int *pBaseFunc4 = (int *)*(vtptr1 + 3);(FUNC(pBaseFunc1))();(FUNC(pBaseFunc2))();(FUNC(pBaseFunc3))();(FUNC(pBaseFunc4))();// .... 后面省略若干输出内容,可自行补充return 0;
}

测试效果:

派生类覆盖基类虚函数

我们再来看一下派生类覆盖了基类的虚函数的情形,可见:

  1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置
  2. 派生类没有覆盖的虚函数延用基类的

代码如下所示,注意这里只给出了类的定义,main函数的测试代码与上节一样:

class Devired : public Base, public Base2, public Base3
{
public:Devired(int mem = 7) : m_iMem1(mem) { ; }virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }virtual void vfunc1() { std::cout << "In Devired vfunc1()" << std::endl; }virtual void vBase2func1() { std::cout << "In Devired vfunc1()" << std::endl; }private:int m_iMem1;
};

测试效果

钻石型虚继承

该继承还是遵循上述的所有原则,我们直接来测试。

测试代码

// 测试四:钻石型虚继承//虚基指针所指向的虚基表的内容:
//  1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
//  2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;class B
{
public:B() : _ib(10), _cb('B') {}virtual void f(){cout << "B::f()" << endl;}virtual void Bf(){cout << "B::Bf()" << endl;}private:int _ib;char _cb;
};class B1 : virtual public B
{
public:B1() : _ib1(100), _cb1('1') {}virtual void f(){cout << "B1::f()" << endl;}#if 1virtual void f1(){cout << "B1::f1()" << endl;}virtual void Bf1(){cout << "B1::Bf1()" << endl;}
#endifprivate:int _ib1;char _cb1;
};class B2 : virtual public B
{
public:B2() : _ib2(1000), _cb2('2') {}virtual void f(){cout << "B2::f()" << endl;}
#if 1virtual void f2(){cout << "B2::f2()" << endl;}virtual void Bf2(){cout << "B2::Bf2()" << endl;}
#endif
private:int _ib2;char _cb2;
};class D : public B1, public B2
{
public:D() : _id(10000), _cd('3') {}virtual void f(){cout << "D::f()" << endl;}#if 1virtual void f1(){cout << "D::f1()" << endl;}virtual void f2(){cout << "D::f2()" << endl;}virtual void Df(){cout << "D::Df()" << endl;}
#endif
private:int _id;char _cd;
};int main(void)
{D d;cout << sizeof(d) << endl;return 0;
}

测试效果

1>class D   size(52):
1>  +---
1> 0    | +--- (base class B1)
1> 0    | | {vfptr}
1> 4    | | {vbptr}
1> 8    | | _ib1
1>12    | | _cb1
1>      | | <alignment member> (size=3)
1>  | +---
1>16    | +--- (base class B2)
1>16    | | {vfptr}
1>20    | | {vbptr}
1>24    | | _ib2
1>28    | | _cb2
1>      | | <alignment member> (size=3)
1>  | +---
1>32    | _id
1>36    | _cd
1>      | <alignment member> (size=3)
1>  +---
1>  +--- (virtual base B)
1>40    | {vfptr}
1>44    | _ib
1>48    | _cb
1>      | <alignment member> (size=3)
1>  +---
1>
1>D::$vftable@B1@:
1>  | &D_meta
1>  |  0
1> 0    | &D::f1
1> 1    | &B1::Bf1
1> 2    | &D::Df
1>
1>D::$vftable@B2@:
1>  | -16
1> 0    | &D::f2
1> 1    | &B2::Bf2
1>
1>D::$vbtable@B1@:
1> 0    | -4
1> 1    | 36 (Dd(B1+4)B)
1>
1>D::$vbtable@B2@:
1> 0    | -4
1> 1    | 20 (Dd(B2+4)B)
1>
1>D::$vftable@B@:
1>  | -40
1> 0    | &D::f
1> 1    | &B::Bf
1>

总结

几个原则

单继承

  1. 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置
  2. 派生类没有覆盖的虚函数就延用基类的。同时,虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。

多继承

  1. 每个基类都有自己的虚函数表
  2. 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后

安全性问题

当我们直接通过父类指针调用子类中的未覆盖父类的成员函数,编译器会报错,但通过实验,我们可以用对象的地址访问到各个子类的成员函数,就违背了C++语义,操作会有一定的隐患,当我们使用时要注意这些危险的东西!

参考:

https://coolshell.cn/articles/12165.html

https://jocent.me/2017/08/07/virtual-table.html

https://blog.csdn.net/lihao21/article/details/50688337

转载于:https://www.cnblogs.com/Mered1th/p/10924545.html

深入理解C++ 虚函数表相关推荐

  1. C++中的多态——理解虚函数表及多态实现原理

    多态及其实现原理 一.多态的概念 概念 构成条件 二.虚函数的重写 重写的定义 重写的特殊情况 override和final关键字 区分重写.重载.重定义 抽象类的概念 三.多态的实现原理 父类对象模 ...

  2. 图解C++虚函数 虚函数表

    图解C++虚函数 2016年07月02日 17:47:17 海枫 阅读数:5181 标签: 虚函数c++g++对象模型C++虚函数更多 个人分类: C/C++/linux 版权声明:本文为博主原创文章 ...

  3. C/C++杂记:虚函数的实现的基本原理 虚函数表

    Malecrab 博客园 首页 新随笔 联系 订阅 管理 C/C++杂记:虚函数的实现的基本原理 1. 概述 简单地说,每一个含有虚函数(无论是其本身的,还是继承而来的)的类都至少有一个与之对应的虚函 ...

  4. C++虚函数表,虚表指针,内存分布

    虚函数表和内存分布那一块转载自:https://blog.twofei.com/496/ 虚函数效率转载自:https://www.cnblogs.com/rollenholt/articles/20 ...

  5. 深入剖析C++多态、VPTR指针、虚函数表

    在讲多态之前,我们先来说说关于多态的一个基石------类型兼容性原则. 一.背景知识 1.类型兼容性原则 类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代.通过公有继承,派 ...

  6. C++ COM编程之接口背后的虚函数表

    前言 学习C++的人,肯定都知道多态机制:多态就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.对于多态机制是如何实现的,你有没有想过呢?而COM中的接口就将这一机制运用 ...

  7. C++ 面向对象(二)多态 : 虚函数、多态原理、抽象类、虚函数表、继承与虚函数表

    目录 多态 多态的概念 多态的构成条件 虚函数 虚函数的重写 协变(返回值不同) 析构函数的重写(函数名不同) final和override final override 重载, 重写, 重定义对比 ...

  8. C 虚函数表及多态内部原理详解

    C 中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态" ...

  9. python虚函数_virtual(虚函数) vtbl(虚函数表)与vptr(虚函数表指针)

    类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址 注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享.类的每个虚成员占据虚函数表中的一行.如果类 ...

最新文章

  1. RabbitMQ Topic交换机的作用
  2. YBTOJ:向量问题(线段树分治、凸包)
  3. [react] 在React中如何引入图片?哪种方式更好?
  4. Broadcast简单使用
  5. java基础-关键字final
  6. 玖云个人导航API工具网站源码
  7. 转载关于使用Ant打包Flex的一些脚本
  8. 手机上有没有学python的软件-有哪些可以在手机上敲Python代码的App
  9. jquery.autocomplete自动完成控件
  10. Base64 Base32 Base16全家桶
  11. C++中数据类型int, short, long, long long的数据范围
  12. 大众点评霸王餐自动报名autojs(更新版)
  13. 活性基因免疫靶向细胞疗法
  14. BI规划落地的正确姿势,五步教你搭建企业级BI项目
  15. android开发笔记之高级主题—传感器的简单介绍
  16. kali linux 清华源_KALI LINUX 2.0 2019 更新国内源
  17. signature=95eea087473d092a96a2e8a1766b9911,黑马旅游网初始项目文件!WEB阶段
  18. ubuntu下安装和配置Qt5.12.8
  19. DynamipsGUI v2.7 出不来idle-pc 值解决方法
  20. early翻译_early是什么意思_early翻译_读音_用法_翻译

热门文章

  1. Python 数据分析三剑客之 Matplotlib(一):初识 Matplotlib 与其 matplotibrc 配置文件
  2. 微博API接入初识【cxn专用】
  3. android布局属性,Android 布局学习之——LinearLayout属性baselineAligned的作用及baseline...
  4. java生成16位随机数_java中如何产生一个16位数字组成的随机字符串?谢谢各位了...
  5. 正则表达式 python_Python正则表达式总结
  6. php xml数据拼接,在PHP中合并XML文件
  7. 基于matlab的信号与系统实例,华南理工大学信号与系统实验基于Matlab的信号处理实例...
  8. UNIX(进程间通信):16深入理解Socket
  9. pySerial -- Python的串口通讯模块
  10. Java设计模式(4 / 23):单例模式