面试题答案
一键面试虚函数表工作原理
- 动态绑定基础:在C++中,虚函数表(vtable)是实现多态性的关键机制。当一个类包含虚函数时,编译器会为该类创建一个虚函数表。虚函数表本质上是一个函数指针数组,每个元素指向该类的一个虚函数的实际实现。
- 对象的vptr:每个包含虚函数的类的对象,都隐含地包含一个指向虚函数表的指针(vptr)。当通过指针或引用调用虚函数时,实际调用的函数是根据对象的动态类型,通过vptr找到虚函数表,然后从虚函数表中获取对应函数指针并调用。例如:
class Base {
public:
virtual void func() { std::cout << "Base::func" << std::endl; }
};
class Derived : public Base {
public:
void func() override { std::cout << "Derived::func" << std::endl; }
};
Base* ptr = new Derived();
ptr->func();
这里ptr
虽然是Base*
类型,但实际指向Derived
对象,调用func
时会通过Derived
对象的vptr找到Derived
的虚函数表,从而调用Derived::func
。
内存布局方式
- 单继承:在单继承且基类有虚函数的情况下,对象内存布局中,vptr通常位于对象内存布局的起始位置,紧接着是对象的数据成员。例如:
class Base {
public:
virtual void func() {}
int data;
};
Base
对象内存布局为:vptr
(指向Base
的虚函数表),然后是data
。
2. 多继承且基类含虚函数:当一个类继承自多个含有虚函数的基类时,内存布局会变得复杂。每个基类都可能有自己的虚函数表。派生类对象的内存布局中,可能会有多个vptr,分别对应不同基类的虚函数表。例如:
class Base1 {
public:
virtual void func1() {}
};
class Base2 {
public:
virtual void func2() {}
};
class Derived : public Base1, public Base2 {
public:
void func1() override {}
void func2() override {}
};
Derived
对象内存布局可能是:Base1
的vptr(指向Base1
虚函数表,其中func1
可能被Derived::func1
覆盖),Base1
的数据成员(如果有),Base2
的vptr(指向Base2
虚函数表,其中func2
可能被Derived::func2
覆盖),Base2
的数据成员(如果有),Derived
自己的数据成员。
通过指针或引用访问虚函数表中的函数
- 通过对象指针:当有一个指向包含虚函数的类对象的指针时,编译器在编译时无法确定实际调用的虚函数版本。运行时,会根据指针所指对象的实际类型,通过对象的vptr找到对应的虚函数表,再从虚函数表中获取函数指针并调用。如上述
Base* ptr = new Derived(); ptr->func();
的例子。 - 通过对象引用:通过对象引用调用虚函数原理与指针类似。引用在底层也是通过指针实现的,同样会根据引用所绑定对象的实际类型,利用vptr找到虚函数表来调用相应虚函数。例如:
Base& ref = *new Derived();
ref.func();
这里ref
绑定Derived
对象,调用func
时会通过Derived
对象的vptr找到正确的虚函数。
注意,直接通过虚函数表指针(vptr)来访问虚函数表中的函数是不推荐且不安全的,因为虚函数表的布局是编译器相关的。但在一些极端的底层调试或研究场景下,可以通过类型转换和指针运算来间接访问,但这种做法非常依赖编译器实现细节。