真实面经题目 · 原创解析
cpp的虚函数以及实现原理?
虚函数是 C++ 实现运行时多态的核心机制。它允许通过基类指针或引用调用派生类重写后的函数,把调用哪个函数的决定从编译期推迟到运行期。主流编译器通常通过虚函数表和对象中的虚表指针实现:多态类有虚表,对象保存指向虚表的指针,虚调用时先取虚表指针,再按固定槽位找到函数地址并间接调用。
真实面经题目 · 原创解析
虚函数是 C++ 实现运行时多态的核心机制。它允许通过基类指针或引用调用派生类重写后的函数,把调用哪个函数的决定从编译期推迟到运行期。主流编译器通常通过虚函数表和对象中的虚表指针实现:多态类有虚表,对象保存指向虚表的指针,虚调用时先取虚表指针,再按固定槽位找到函数地址并间接调用。
C++ 虚函数用于实现运行时多态。基类把成员函数声明为 virtual,派生类用相同签名重写它,当通过基类指针或引用调用该函数时,实际执行对象真实动态类型对应的版本,而不是指针或引用静态类型对应的版本。主流实现会为含虚函数的类生成虚函数表,表里保存各个虚函数最终调用地址;每个多态对象内部通常有隐藏的虚表指针,指向自己动态类型对应的虚表。调用虚函数时,程序从对象中取虚表指针,按虚函数固定槽位取出函数地址并间接调用。关键点包括:通过指针或引用调用才体现动态绑定;派生类重写签名要匹配,建议用 override;基类用于多态删除时析构函数必须 virtual;构造和析构期间不要依赖完整多态;虚函数有对象体积和间接调用开销,但换来接口和实现解耦。
虚函数解决同一个接口在不同对象上表现出不同行为的问题。没有虚函数时,成员函数调用通常由表达式静态类型决定,编译器在编译期确定调用目标。基类指针指向派生类对象时,如果调用非虚函数,仍按基类类型选择函数。虚函数把绑定推迟到运行期,使调用目标取决于对象真实动态类型。
虚函数通常需要三个条件同时成立:基类函数声明为 virtual;派生类提供匹配签名的重写版本;调用发生在基类指针或基类引用上。如果普通对象直接调用,表达式静态类型明确,编译器往往可以直接绑定。动态绑定关心对象实际是什么类型,而不是变量声明成什么类型。
主流实现会为每个包含虚函数的类维护虚函数表。虚表本质上是函数地址数组或类似结构,每个虚函数在表中有固定槽位。基类声明的虚函数占据一个槽位,派生类重写时在派生类虚表中用派生类函数地址替换原槽位;新增虚函数通常增加新槽位。虚调用时先取对象虚表指针,再取槽位函数地址。
含虚函数的对象通常多出隐藏的虚表指针,指向该对象当前动态类型对应的虚表。这不是 C++ 标准强制的语法概念,但主流 ABI 和编译器都常这样实现。因为对象要保存虚表指针,多态类对象通常比无虚函数对象多一个指针大小的空间开销。多继承和虚继承下可能出现多个虚表指针和更复杂布局。
重写要求派生类函数与基类虚函数在函数名、参数列表、cv 限定、引用限定等方面匹配,返回值也必须相同或满足协变返回规则。如果参数不同,不是重写,而是重载或隐藏。同名但签名不匹配的函数可能隐藏基类同名函数。使用 override 可以让编译器检查是否真的覆盖了基类虚函数。
如果一个类被设计为多态基类,并且可能通过基类指针删除派生类对象,基类析构函数必须是 virtual。否则 delete 基类指针时可能只按基类析构逻辑处理,派生类析构函数不会被正确调用,资源释放不完整,行为属于未定义。虚析构保证销毁对象时从最派生类开始按正确顺序析构。
构造函数和析构函数中调用虚函数容易出错。构造基类部分时,派生类部分尚未构造完成,因此虚调用不会派发到派生类版本,而是解析为当前构造阶段所属类的版本。析构时派生类部分已经开始或完成析构,继续派发到派生类函数可能访问失效成员,所以同样不应依赖完整对象多态。
虚函数开销主要包括对象中额外虚表指针、类的虚表存储、调用时一次间接寻址,以及间接调用可能削弱内联和分支预测。实际开销要结合场景判断,业务代码中通常可接受,性能关键路径和大量对象场景才更敏感。现代编译器在能证明动态类型时也可能去虚化,优化成直接调用甚至内联。
虚函数可以有默认实现,派生类可以选择重写;纯虚函数表达接口约束,使类通常成为抽象类,不能直接实例化。纯虚函数也可以在类外提供实现,但派生类仍需要实现它才能成为可实例化的具体类。
当基类指针指向派生类对象并通过该指针 delete 时,只有虚析构才能保证先调用派生类析构函数,再调用基类析构函数。否则派生类部分可能无法释放资源,并且行为未定义。
通常虚函数表是每个类一份或每个类型相关布局一份,而不是每个对象一份。对象中保存的是指向虚函数表的虚表指针,多个同类型对象通常共享同一张虚表。
不能。虚函数依赖对象的动态类型和对象中的虚表指针,而静态成员函数不绑定具体对象,也没有 this 指针,因此没有运行时多态派发基础。
构造函数不能是虚函数。对象构造前还没有形成完整动态类型,虚表指针也处于构造过程中的阶段性状态,无法通过虚机制决定应该构造哪一个类型。创建对象的具体类型必须在构造开始前已经确定。
可以声明为虚函数,但是否真正内联取决于调用场景和编译器优化。通过基类指针或引用动态派发时通常难以内联;如果编译器能确定实际类型,仍可能去虚化并内联。