前言

继承、封装、多态是C++作为OO语言的三大特性。在学习C++的过程中,我们都对虚函数机制实现多态有或多或少的了解。尽管在日常的编程中,我们可能掌握了虚函数的特性并熟练地将其运用在项目中又或者根本搞不来C++而对虚函数望而生畏。别慌!本文将从底层揭秘虚函数究竟是怎么操作的,在运行过程中究竟执行的是什么样的代码。话不多说,搞快点!

环境

  • 操作系统:macOS Mojave 10.14.5
  • 编译器:Apple LLVM version 10.0.1 (clang-1001.0.46.4)
  • 工具:Hopper Disassembler v4

正文

虚函数是啥?

class A
{
public:int a;int b;virtual void f() {}
};class B : public A
{
public:int x;int y;void f() override {}
};int main()
{B b;A *x = &b;x->f();return 0;
}

先让我们来看看简单的一段代码,main函数中实例化了一个派生类B的对象,然后使用基类指针去指向该对象,当该指针调用f成员的时候,调用的并不是基类A的成员函数而是派生类B的成员函数。原来如此!基类指针似乎能够根据其真正指向的对象类型来调用实际重载过的函数,这就是虚函数机制嘛!!?

那么问题来了,这到底是如何实现的?可以通过编译器静态编译实现嘛?答案是不行的,比如我们随手写一手辣鸡代码。

... // 重用上述 class A B
int main()
{int x;A* p;A  a;B  b;while(cin >> x){p = x > 0 ? &a : &b;p->f();}return 0;
}

我们无法在运行前知晓p所指向的真正类型,因此必须有合适的方法来解决这一问题。我们在学习过程中也听说过虚函数表(vtable)、动态绑定(dynamic binding)等名词,据说是用来实现虚函数的,那么其中的魔法究竟是怎么样的呢?让我们来深入了解一下!

虚函数表是啥?

之前我们就一直提到要深入了解深入了解,那么究竟是多深入呢?那自然是要通过反汇编来瞧一瞧啦!这里我们使用最开始的一段代码来分析。

main函数

首先我们注意到地址为0x0000000100000edf的指令lea rdi, qword [rbp+var_20],这条指令等价于rdi = rbp+var_20rbp+var_20是一个内存地址,而[rbp+var_20]代表该地址上的内容,qword指的是这个地址开始的包含8字节的内存空间),而这个地址正是B b的地址。下面我们进入派生类B的构造函数中观察构造函数(call __ZN1BC1Ev)如何构造这一对象。(mov指令可以理解为把右边的值赋给左边)

这里先关注一下此时堆栈的情况:

                   _____________________________|                             |                      高地址
rbp ----->        |                             |                        ||_____________________________|                        ||                             |                        |    堆栈向下增长|                             |                        ||_____________________________|                      \ | /|                             |                       \ /|                             |                      低地址       |_____________________________|          |                             |
address of B b -> |                             |        左侧一个空间8字节,假设最底下地址为|_____________________________|        0x1000, 那么上一个格子的地址为0x1008 |                             ||                             ||_____________________________

class B的构造函数

第一个函数似乎没有什么实质性的作用,因此我们进入下一个函数(call __ZN1BC2Ev

注意此时rdi寄存器中存储的是B b的地址。我们注意到地址为0x0000000100000f38~0x0000000100000f47的指令执行后的效果是rax = rdi = address of B b。由于类B派生自类A,因此会调用A的构造函数,因此我们进入A的构造函数来看一看。

class A的构造函数

注意此时rdi寄存器中存储仍然的是B b的地址。首先我们观察到地址为0x0000000100000f87的指令mov qword [rdi], rax,是将寄存器rax中的值赋给了B b的首8个字节,那么寄存器rax中存的是啥?我们看到前面有两条指令mov rax, qword [0x100001000]以及add rax, 0x10,于是我们追踪地址0x10000100,看看相应的内存块中存的是什么。

我们发现其存放的依然是一个地址(可以理解为C语言中的指针),于是我们继续挖掘下去。

class A的vtable

wow!我们看到反汇编软件上表明这是基类A的vtable(虚函数表),而rax += 0x10所指向的地址为0x0000000100001068,我们发现前8个字节似乎有点玄机,取出来瞧瞧,由于我们的机器上遵循的是小端表示(little endian),因此应该是0x0000000100000fa0,似乎又是一个地址,我们继续追踪。

A::f()

于是我们来到了基类A的成员函数f,原来虚函数表中存储的是该函数的地址!不过别慌,我们还没有结束。构造完A后,我们继续分析B的构造函数。

此前的堆栈情况发生变化:

                   _____________________________|                             |                      高地址
rbp ----->        |                             |                        ||_____________________________|                        ||                             |                        |    堆栈向下增长|                             |                        ||_____________________________|                      \ | /|                             |                       \ /|                             |                      低地址        |_____________________________|        左侧一个空间8字节,假设最底下地址为|                             |        0x1000, 那么上一个格子的地址为0x1008
address of B b -> |   address of A's vtable     |_________|_____________________________|         ||                             |         ||                             |         ||_____________________________|         |         ______________________|         |                     ||         |                     ||         |_____________________|--------->|                     ||  address of A::f    ||_____________________|

class B的构造函数

我们在B的构造函数中同样发现像了类似的赋值情况,因此我们断定这是在操作B的vtable(虚函数表),于是继续追踪验证我们的猜想。

追踪地址0x0000000100001010

class B的vtable

果不其然,我们发现了B的vtable(虚函数表),同样地址0x0000000100000f90指向的是B的成员函数f

B::f()

class B的构造函数

继续回到B的构造函数中,由于B的构造函数中,在执行A的构造函数之前,我们执行了mov qword [rbp + var_10], rdi的存储指令,因此在执行mov qword [rbp + var_10], rdi后,rdi寄存器指对象B b的地址。

此前的堆栈情况继续变化(这里我们标注出成员变量的布局):

                   _____________________________|                             |                      高地址
rbp ----->        |                             |                        ||_____________________________|                        ||                             |                        |    堆栈向下增长|      x      |       y       |                        ||_____________________________|                      \ | /|                             |                       \ /|      a      |       b       |                      低地址        |_____________________________|        左侧一个空间8字节,假设最底下地址为|                             |        0x1000, 那么上一个格子的地址为0x1008
address of B b -> |   address of B's vtable     |_________           |_____________________________|         ||                             |         |       在我的机器上int占4字节|                             |         ||_____________________________|         |         ______________________|         |                     ||         |                     ||         |_____________________|--------->|                     ||  address of B::f    ||_____________________|

虚函数如何执行?

分析完虚函数表,让我们来看一看汇编代码中虚函数究竟被如何表达?

main函数

依然回到main函数中,我们继续向下分析:首先还是需要获得B b的地址,指令lea rdi, qword[rbp+var_20] mov qword[rbp+var_28], rdi即对应代码A* x = &b;,获得地址并存放在局部变量中(栈上空间),然后关键的一步来了,指令mov rax, qword [rdi],获得B b的首8个字节,我们回忆起之前得到的布局图,首8个字节不就是虚函数表的地址!然后看到指令call qword [rax],一下子恍然大悟,虚函数表中的第一项不就是B的成员函数f的地址!也就是说,虚函数的机制实际上是通过在对象中存储的虚函数表的地址,追踪到编译生成出来附带在生成的可执行文件中某处的虚函数表,然后根据其中记录的具体的函数地址来实现调用的啊!

何时会触发虚函数机制?

  • 使用指针

在上述的探索中,我们正是使用了这个方法,此外像下面这样调用也会触发虚函数机制。

... // 重用上述class A B
int main()
{B b;A* x = &b;(*x).f();return 0;
}
  • 使用引用
... // 重用上述class A B
int main()
{B b;A& x = b;x.f();return 0;
}

我们照例来看一下这段代码的汇编代码。

我们发现使用引用的情况下,我们仍然触发了虚函数的机制。

那么直接调用会是什么情况?整上代码!

... // 重用上述class A B
int main()
{B b;b.f();return 0;
}

我们发现汇编代码直接调用成员函数。

这也回答了为什么虚函数机制会带来更多的开销,当触发虚函数时,首先要取得存储在对象中的虚函数表的地址,这里需要一次内存寻址,然后通过该地址取得真正要调用的函数的地址,这里又是一次内存寻址,之后才开始进行真正的函数调用。因此我们不能过分依赖于虚函数机制,在类型明确的情况下更适合于直接调用。

小结

虚函数的实现依赖的是一张虚函数表(vtable),由编译器编译生成并存放在某处,程序运行时在构造对象时会将该地址存放在对象中。在真正调用的时候会先通过存储在对象中的虚函数表的地址,寻找得到真正需要调用的成员函数的地址。

转载:探索C++虚函数的实现 - 知乎 (zhihu.com)

C++:虚函数的实现相关推荐

  1. C++ 虚函数与存虚函数

    什么是虚函数: 虚函数 是在基类中使用关键字 virtual 声明的函数,在C++ 语言中虚函数可以继承,当一个成员函数被声明为虚函数之后,其派生类中的同名函数都自动生成为虚函数, 虚函数主要体验C+ ...

  2. 【C++】多态(早期绑定、后期绑定)、抽象类(纯虚函数)、虚析构函数

    我们都知道面向对象编程的三大特征是封装.继承.多态,今天我们就来说一下其中之一的多态. 概念: 多态: 多态字面意思就是多种形态,C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同 ...

  3. C#中虚函数,抽象,接口的简单说明

    虚函数:由virtual声明,它允许在派生类中被重写,要重写方法,必须先声名为virtual public class myclass { public virtual int myint() { 函 ...

  4. 提高C++性能的编程技术笔记:虚函数、返回值优化+测试代码

    虚函数:在以下几个方面,虚函数可能会造成性能损失:构造函数必须初始化vptr(虚函数表):虚函数是通过指针间接调用的,所以必须先得到指向虚函数表的指针,然后再获得正确的函数偏移量:内联是在编译时决定的 ...

  5. 但并不从包含函数声明的接口派生_C++的虚函数和纯虚函数

    虚函数:类成员函数前面添加virtual关键字,则该函数被称为虚函数. 纯虚函数:在虚函数的基础上,在函数末尾加上 = 0. class Animal {public: virtual void Sh ...

  6. c++ 虚函数_到底什么情况下会合成默认构造函数?

    来源:https://www.cnblogs.com/QG-whz/p/4676481.html 作者:good luck 编辑:公众号[编程珠玑] 编辑注:没有构造函数的时候编译器一定会生成默认构造 ...

  7. C++——虚函数(Virtual Member Functions) 【functions语意学】

    单继承下的虚函数 虚函数的实现: 为每个有虚函数的类配一张虚函数表(virtual table),它存储该类类型信息和所有虚函数执行期的地址. 为每个有虚函数的类插入一个指针(vptr),这个指针指向 ...

  8. 一口气搞懂《虚函数和纯虚函数》

    学习C++的多态性,你必然听过虚函数的概念,你必然知道有关她的种种语法,但你未必了解她为什么要那样做,未必了解她种种行为背后的所思所想.深知你不想在流于表面语法上的蜻蜓点水似是而非,今天我们就一起来揭 ...

  9. 虚函数实现的基本原理(转载)

    1.概述 每一个含有虚函数(无论是其本身就含有的,还是从基类继承过来的)的类都至少有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针.如下图所示 : 其中: B的虚函数表中存放着B:: ...

  10. C++中虚函数可以是内联函数吗?

    1.需要注意的几点: 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联. 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因 ...

最新文章

  1. CoreOS 和 Kubernetes 1.5 自主运行 Kubernetes、Container Linux
  2. java商品新增怎麽弄_添加新商品时如何初始化计数器 - java
  3. 极客大佬用什么电脑_极客特惠:笔记本电脑,高清电视和免费应用
  4. Opencv与dlib联合进行人脸关键点检测与识别
  5. rest风格的get加密字符串怎么接收_RESTful Api的设计与风格,你该学一下咯
  6. 0基础半路转行学IT还来得及吗?
  7. 【好文推荐】梁宁:人一通透,就不怎么算小账
  8. 机器视觉:偏振镜光学原理和在机器视觉中的应用
  9. memtrack: Couldn‘t load memtrack module (No such file or directory) 的问题解决
  10. 【数字信号去噪】基于matlab遗传算法优化变分模态分解VMD数字信号去噪(目标函数为样本熵)【含Matlab源码 1982期】
  11. VOS防盗打,防攻击的一些看法
  12. OpenDrive里XY和ST
  13. ListView 优化之 ViewHolder 复用机制
  14. html 组件化 编辑器,纯前端表格控件SpreadJS V14.0发布:组件化编辑器+数据透视表...
  15. 康佳电视系统升级服务器地址,康佳电视各平台升级方法及强制刷机汇总
  16. python查找excel中重复数据_python中查找excel某一列的重复数据剔除之后打印
  17. 政务内网、政务外网、政务专网
  18. Insert Guest Additions CD image 没有反应
  19. CC2530串口命令控制LED灯开关
  20. 帝国cms更新php,帝国CMS自动刷新首页的方法

热门文章

  1. 如何才能做好企业内部客服知识管理?
  2. [转]2008年最牛语录
  3. css伪类元素实现小圆点效果
  4. CSS 画圆 三角形箭头
  5. java线程锁有哪几种_java有哪些锁种类
  6. Android Studio 真机调试步骤
  7. 设置form提交的编码
  8. Windows 如何卸载 Docker
  9. 性能测试-socket协议
  10. 情人节该送什么?超受欢迎的音质蓝牙耳机盘点!