真实面经题目 · 原创解析

cpp的虚函数以及实现原理?

虚函数是 C++ 实现运行时多态的核心机制。它允许通过基类指针或引用调用派生类重写后的函数,把调用哪个函数的决定从编译期推迟到运行期。主流编译器通常通过虚函数表和对象中的虚表指针实现:多态类有虚表,对象保存指向虚表的指针,虚调用时先取虚表指针,再按固定槽位找到函数地址并间接调用。

出现于:阿里巴巴 · 后端开发

60 秒回答模板

C++ 虚函数用于实现运行时多态。基类把成员函数声明为 virtual,派生类用相同签名重写它,当通过基类指针或引用调用该函数时,实际执行对象真实动态类型对应的版本,而不是指针或引用静态类型对应的版本。主流实现会为含虚函数的类生成虚函数表,表里保存各个虚函数最终调用地址;每个多态对象内部通常有隐藏的虚表指针,指向自己动态类型对应的虚表。调用虚函数时,程序从对象中取虚表指针,按虚函数固定槽位取出函数地址并间接调用。关键点包括:通过指针或引用调用才体现动态绑定;派生类重写签名要匹配,建议用 override;基类用于多态删除时析构函数必须 virtual;构造和析构期间不要依赖完整多态;虚函数有对象体积和间接调用开销,但换来接口和实现解耦。

考点 虚函数解决的问题
主线 动态绑定条件
易错点 把虚函数简单理解成加了 virtual 的普通函数,没…

深入解析

01

虚函数解决的问题

虚函数解决同一个接口在不同对象上表现出不同行为的问题。没有虚函数时,成员函数调用通常由表达式静态类型决定,编译器在编译期确定调用目标。基类指针指向派生类对象时,如果调用非虚函数,仍按基类类型选择函数。虚函数把绑定推迟到运行期,使调用目标取决于对象真实动态类型。

02

动态绑定条件

虚函数通常需要三个条件同时成立:基类函数声明为 virtual;派生类提供匹配签名的重写版本;调用发生在基类指针或基类引用上。如果普通对象直接调用,表达式静态类型明确,编译器往往可以直接绑定。动态绑定关心对象实际是什么类型,而不是变量声明成什么类型。

03

虚函数表机制

主流实现会为每个包含虚函数的类维护虚函数表。虚表本质上是函数地址数组或类似结构,每个虚函数在表中有固定槽位。基类声明的虚函数占据一个槽位,派生类重写时在派生类虚表中用派生类函数地址替换原槽位;新增虚函数通常增加新槽位。虚调用时先取对象虚表指针,再取槽位函数地址。

04

对象中的虚表指针

含虚函数的对象通常多出隐藏的虚表指针,指向该对象当前动态类型对应的虚表。这不是 C++ 标准强制的语法概念,但主流 ABI 和编译器都常这样实现。因为对象要保存虚表指针,多态类对象通常比无虚函数对象多一个指针大小的空间开销。多继承和虚继承下可能出现多个虚表指针和更复杂布局。

05

重写、隐藏与重载

重写要求派生类函数与基类虚函数在函数名、参数列表、cv 限定、引用限定等方面匹配,返回值也必须相同或满足协变返回规则。如果参数不同,不是重写,而是重载或隐藏。同名但签名不匹配的函数可能隐藏基类同名函数。使用 override 可以让编译器检查是否真的覆盖了基类虚函数。

06

虚析构函数

如果一个类被设计为多态基类,并且可能通过基类指针删除派生类对象,基类析构函数必须是 virtual。否则 delete 基类指针时可能只按基类析构逻辑处理,派生类析构函数不会被正确调用,资源释放不完整,行为属于未定义。虚析构保证销毁对象时从最派生类开始按正确顺序析构。

07

构造析构期间的虚调用

构造函数和析构函数中调用虚函数容易出错。构造基类部分时,派生类部分尚未构造完成,因此虚调用不会派发到派生类版本,而是解析为当前构造阶段所属类的版本。析构时派生类部分已经开始或完成析构,继续派发到派生类函数可能访问失效成员,所以同样不应依赖完整对象多态。

08

性能与优化影响

虚函数开销主要包括对象中额外虚表指针、类的虚表存储、调用时一次间接寻址,以及间接调用可能削弱内联和分支预测。实际开销要结合场景判断,业务代码中通常可接受,性能关键路径和大量对象场景才更敏感。现代编译器在能证明动态类型时也可能去虚化,优化成直接调用甚至内联。

易错点

  • 把虚函数简单理解成加了 virtual 的普通函数,没有说明运行时多态和动态绑定。
  • 误以为每个对象都有一整张虚函数表,实际上对象通常只有虚表指针,虚表一般由同类对象共享。
  • 认为派生类只要函数名相同就是重写,忽略参数、const 限定、引用限定和返回类型规则。
  • 忘记说明通过基类指针或引用调用才是典型动态派发场景。
  • 认为构造函数可以是虚函数,或者在构造函数中调用虚函数会派发到派生类版本。
  • 忽略虚析构函数,导致无法解释通过基类指针删除派生类对象时的风险。
  • 把重载、隐藏、重写混为一谈,不能准确解释 override 的价值。
  • 认为虚函数一定很慢,忽略编译器去虚化、内联优化以及实际业务场景权衡。

面试官追问

虚函数和纯虚函数有什么区别?

虚函数可以有默认实现,派生类可以选择重写;纯虚函数表达接口约束,使类通常成为抽象类,不能直接实例化。纯虚函数也可以在类外提供实现,但派生类仍需要实现它才能成为可实例化的具体类。

为什么析构函数经常要声明为 virtual?

当基类指针指向派生类对象并通过该指针 delete 时,只有虚析构才能保证先调用派生类析构函数,再调用基类析构函数。否则派生类部分可能无法释放资源,并且行为未定义。

虚函数表是每个对象一份还是每个类一份?

通常虚函数表是每个类一份或每个类型相关布局一份,而不是每个对象一份。对象中保存的是指向虚函数表的虚表指针,多个同类型对象通常共享同一张虚表。

静态函数能不能是虚函数?

不能。虚函数依赖对象的动态类型和对象中的虚表指针,而静态成员函数不绑定具体对象,也没有 this 指针,因此没有运行时多态派发基础。

构造函数能不能是虚函数?

构造函数不能是虚函数。对象构造前还没有形成完整动态类型,虚表指针也处于构造过程中的阶段性状态,无法通过虚机制决定应该构造哪一个类型。创建对象的具体类型必须在构造开始前已经确定。

内联函数能不能是虚函数?

可以声明为虚函数,但是否真正内联取决于调用场景和编译器优化。通过基类指针或引用动态派发时通常难以内联;如果编译器能确定实际类型,仍可能去虚化并内联。