# 虚函数的实现机制
面试官问:"虚函数是怎么实现的?调用一个虚函数的时候底层发生了什么?"
这题考的不是"什么是多态",而是多态背后的实现细节。很多人能说出"虚函数表"三个字就卡住了,说不清vtable在哪、vptr什么时候初始化、调用链路是怎么走的。把这条链路讲清楚,这题就拿满分。
# 简要回答
编译器为每个含虚函数的类生成一张虚函数表(vtable),表里存的是该类各虚函数的地址。每个对象内部有一个隐藏的虚指针(vptr),指向所属类的vtable。通过基类指针调用虚函数时,运行时沿着 对象 → vptr → vtable → 函数地址 这条链路找到真正要调用的函数,这就是动态绑定。
# 详细回答
vtable:每个类一张表
编译器看到一个类里有virtual函数,就会为这个类生成一张vtable。表里按声明顺序存放各虚函数的地址。派生类如果override了某个虚函数,它的vtable里对应位置就换成自己的函数地址;没override的槽位继续指向基类版本。
vptr:每个对象一个指针
每个含虚函数的对象,内存布局里都多一个隐藏成员——vptr。构造函数执行时,编译器自动把vptr设置为指向当前类的vtable。所以Base* p = new Derived(),p指向的对象里的vptr指向的是Derived的vtable,不是Base的。
调用链路
p->show() 在底层展开为:取对象的vptr → 在vtable中按偏移找到show的地址 → 跳转调用。整个过程多了一次指针解引用(间接寻址),这就是虚函数比普通函数调用慢一点的原因。
核心就一句话:vtable让"调哪个函数"变成了运行时查表,而不是编译时写死。

# 知识拓展
面试官可能追问:
Q1: vptr放在对象的什么位置?
主流实现(GCC/Clang/MSVC)都把vptr放在对象内存的最前面,这样通过对象首地址就能直接拿到vptr,不需要额外偏移计算。
Q2: 多继承时vtable怎么处理?
多继承时一个对象可能有多个vptr,每条继承链一个,对应多张vtable。通过不同基类指针调用虚函数时,走的是不同的vptr和vtable。
Q3: 虚函数调用比普通函数慢多少?
多一次指针解引用,通常就是一条mov加一条间接call的开销。现代CPU有分支预测和缓存,实际性能差距很小。真正的代价是编译器无法内联虚函数调用,丧失了内联优化的机会。
Q4: 为什么基类析构函数要声明为virtual?
delete basePtr时,如果析构函数不是virtual,只会调用基类的析构函数,派生类的析构不会执行,派生类申请的资源就泄漏了。声明为virtual后,析构也走vtable查表,能正确调到派生类析构。
评论
验证登录状态...