虚函数地址问题的 vcall 引入
vcall 是什么?
vcall
是编译器生成的内容,完整的名字应该叫 vcall thunk
。当通过基类指针或引用调用虚函数时,编译器会通过虚函数表找到实际要调用的函数地址。这种机制被称为虚函数调用(vcall)。
示例代码
以下是一个示例,展示了虚函数调用的工作原理:
#include <iostream>
class Base {
public:
virtual void myFunction() {
std::cout << "Base::myFunction" << std::endl;
}
};
class Derived : public Base {
public:
void myFunction() override {
std::cout << "Derived::myFunction" << std::endl;
}
};
int main() {
Derived myacls;
myacls.myFunction(); // 直接调用,不通过虚函数表
Base* pmyacls = &myacls;
pmyacls->myFunction(); // 通过虚函数表查找并调用
Base& ref = myacls;
ref.myFunction(); // 通过虚函数表查找并调用
myacls.Base::myFunction(); // 明确调用基类的虚函数,等价于调用普通成员函数
return 0;
}
解释
- 直接调用:
myacls.myFunction()
直接调用派生类的函数,不通过虚函数表。 - 通过指针调用:
pmyacls->myFunction()
通过基类指针调用虚函数,编译器会通过虚函数表指针找到虚函数表,然后通过虚函数表找到实际要调用的函数地址。 - 通过引用调用:
ref.myFunction()
通过基类引用调用虚函数,过程与通过指针调用类似。 - 明确调用基类函数:
myacls.Base::myFunction()
明确调用基类的虚函数,等价于调用普通成员函数,不通过虚函数表。
虚函数调用的本质
- 虚函数表指针(vptr):每个对象实例都有一个虚函数表指针,指向其类的虚函数表。
- 虚函数表(vtable):虚函数表中存储了该类的虚函数地址。
- 虚函数调用(vcall):通过基类指针或引用调用虚函数时,编译器会通过对象的虚函数表指针找到虚函数表,然后通过虚函数表找到实际要调用的函数地址。
vcall thunk 的作用
- 调整
this
指针:在多重继承的情况下,例如一个子类继承父类1和父类2,如果父类2的指针要调用子类的析构函数,那这个父类2的指针就需要利用vcall thunk
机制调整this
指针。 - 跳到真正的虚函数中去:
vcall thunk
负责跳转到实际的虚函数实现。
总结
虚函数调用通过虚函数表和虚函数表指针实现多态性。编译器在运行时通过虚函数表找到实际要调用的函数地址,从而实现动态绑定。这种机制使得派生类可以重写基类的虚函数,并通过基类指针或引用调用派生类的实现。vcall thunk
进一步增强了这种机制,通过调整 this
指针和跳转到实际的虚函数实现,确保了多重继承和复杂继承结构中的正确性。