前言
继承、封装、多态是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_20
(rbp+var_20
是一个内存地址,而[rbp+var_20]
代表该地址上的内容,qword
指的是这个地址开始的包含8字节的内存空间),而这个地址正是B b
的地址。下面我们进入派生类B的构造函数中观察构造函数(call __ZN1BC1Ev
)如何构造这一对象。(mov
指令可以理解为把右边的值赋给左边)
这里先关注一下此时堆栈的情况:
_____________________________
| | 高地址
rbp -----> | | |
|_____________________________| |
| | | 堆栈向下增长
| | |
|_