Virtual 机制
这篇文章会尝试使用GDB
来分析C++中虚函数的实现机制。希望可以帮助你更加透彻的理解C++的虚函数实现。
我们用来测试的程序
#include <iostream>
using namespace std;
struct Simple {
int one;
};
struct Base {
virtual void v1() {
cout << "Base::V1" << endl;
}
virtual void v2() {
cout << "Base::V2" << endl;
}
int one;
};
struct Derived : Base {
void v1() override {
cout << "Derived::v1" << endl;
}
};
int main() {
Base* derived = new Derived();
Base* derived1 = new Derived();
Base* base = new Base();
Base* base1 = new Base();
Simple* simple = new Simple();
derived->v1();
derived->v2();
}
下面我们将代码进行编译后,然后使用gdb进行分析
g++ virtual.cc --std=c++11 -g
gdb a.out
我们首先分别看一下derived
,derived1
,base
,base1
,simple
中的内容
variable name | address |
---|---|
derived | 0x55555556aeb0 |
derived1 | 0x55555556aed0 |
base | 0x55555556aef0 |
base1 | 0x55555556af10 |
simple | 0x55555556af30 |
从这两张图,我们可以发现如下几件事
- 当一个class有虚函数时,该class的对象中会有一个
vptr
. - 该
vptr
的大小为8byte(0x55555556aeb8 - 0x55555556aeb0) - 该
vptr
所指向的内容仅与class的类型有关,与对象无关 (derived.vptr == derived.vptr1)
我们下面以derived为例,看一下vptr
所指向的内容。
我们可以看到vptr
指向了一些东西,但具体是什么我们还不知道,但是我们可以发现这个地址的值0x5555555553a6
(小端写法)好像是一个地址,那么我们可以查看一下这个地址指向的是什么。
结果很明显,这里面的值指向的是函数Derived::v1
的定义,我们可以通过这个地址对该函数进行调用。我们再看一下其他的值。
所以结论很清楚,当你的class中含有虚函数时,编译器会为该类创建一个专属的vtable
,vtable
中存放着各个虚函数的实现,如果该类有自己的实现,那么指向的就是它自己的实现,否则指向父类的实现。然后当你创建一个类的对象时,编译器会将指向该vtable
的指针给到对象的vptr
中。
我们最后再看一下,调用的过程。
derived->v1();
derived->v2();
其中rbp
为栈帧,其中-0x38(%rbp)
为获取derived
的地址,即0x55555556aeb0
,也就是vptr
的地址,随后通过mov (%rax),%rax
得到vtable
的地址并保存在%rax
中,因为调用的函数不同,因此derived->v2();
的汇编需要将%rax + 8
得到对应的地址。然后通过mov (%rax),%rdx
得到需要调用的函数地址,最后通过call *%rdx
完成多态的函数调用。
总结
当当一个class有虚函数时,编译器会为该class对象生成一个vptr
,该vptr
的大小为8byte,所指向的内容仅与class的类型有关,与对象无关,这里面的值指向的是函数Derived::v1
的定义,我们可以通过这个地址对该函数进行调用。当实际调用时,编译器会根据你调用的函数不同,调整vtable所指的entry,最后根据entry
项中的地址,完成函数调用。