【C++ 面试真题】聊聊 C++ 的虚函数和多态

【C++ 面试真题】聊聊 C++ 的虚函数和多态

虚函数是 C++ 面试的"分水岭"------背得出"virtual 实现多态"只是及格,真考你的是"虚函数表长什么样、对象内存里存了什么、构造析构里调虚函数会发生什么"。一道题能从语法一路问到内存布局。本文把虚函数表、动态多态、纯虚函数串成一条线,一次讲透。


一、先说结论:虚函数 = 运行期才决定的调用

❓ 什么是虚函数?它和多态什么关系?

✅ 一句话:虚函数是一种"运行期才决定调用哪个版本"的成员函数 ,它是 C++ 实现动态多态的机制。

普通成员函数的调用,在编译期就定死;虚函数的调用,要到运行期根据对象实际类型来决定------这就是多态。

cpp 复制代码
struct Animal {
    virtual void speak() { }
    virtual ~Animal() = default;
};
struct Dog : Animal {
    void speak() override {
        std::cout << "汪";
    }
};
cpp 复制代码
Animal* a = new Dog;
// 运行期才知道 a 指向 Dog
a->speak();  // 打印"汪"
// 这就是多态

a 的静态类型是 Animal*,但实际指向 Dog 对象。没有 virtual,a->speak() 会调 Animal::speak;有了 virtual,它会调 Dog::speak这就是动态多态


二、虚函数表:多态是怎么实现的?

❓ 虚函数到底是怎么实现"运行期决定调用"的?

✅ 靠虚函数表(vtable)虚表指针(vptr)。这是这题的硬核,必须讲清。

机制分三层:

  1. 每个有虚函数的类 ,编译器给它生成一张虚函数表------一个存放虚函数地址的数组;

  2. 每个该类的对象 ,内存里多一个虚表指针 vptr,指向自己所属类的虚表;

  3. 虚函数调用时,通过对象的 vptr 找到虚表,再查表调到正确版本。

    Dog 对象的内存布局:
    +----------+
    | vptr ----+---> Dog 的虚函数表
    +----------+ +------------+
    | 成员... | | &Dog::speak|
    +----------+ +------------+

cpp 复制代码
Animal* a = new Dog;
// a->speak() 实际做的事:
// 1. 通过 a 找到对象里的 vptr
// 2. vptr 指向 Dog 的虚表
// 3. 查表得到 Dog::speak 的地址
// 4. 调用它

💡 加分点(谁建表、谁设 vptr) :虚表是编译期 就生成好的(每个类一张,静态数据);vptr 是构造函数里设置/更新的------对象构造时,vptr 指向自己正在构造的那个类。这就引出后面"构造函数里调虚函数"的坑。


三、静态多态 vs 动态多态

❓ 多态只有虚函数这一种吗?

✅ 不是。C++ 有两种多态

类型 时机 机制 例子
静态多态 编译期 函数重载、模板 max(1,2) vs max(1.0,2.0)
动态多态 运行期 虚函数 a->speak()
cpp 复制代码
// 静态多态:编译期定死
template<typename T>
T add(T a, T b) { return a + b; }
add(1, 2);      // 调 int 版本
add(1.0, 2.0);  // 调 double 版本

// 动态多态:运行期才定
Animal* a = new Dog;
a->speak();  // 运行期查虚表

🎯 区别 :静态多态零开销 (编译期就定,没查表),但灵活性低;动态多态有开销(要查虚表、间接调用),但能在运行期根据实际类型决定行为。面试常问"用模板还是虚函数",本质就是这两者的权衡。


四、构造/析构里调虚函数:经典大坑

❓ 在构造或析构函数里调用虚函数,会怎样?

不会发生多态------调到的是当前类的版本,不是子类的。这是 C++ 最经典的坑之一。

原因:对象的 vptr 在构造过程中逐层设置。构造基类时,vptr 指向基类;进入子类构造后,vptr 才更新指向子类。析构则反过来。

cpp 复制代码
struct Base {
    Base() {
        // 此时 vptr 指向 Base
        init();  // 调 Base::init
        // 不是 Der::init!
    }
    virtual void init() { }
    virtual ~Base() = default;
};
struct Der : Base {
    void init() override { }
};
cpp 复制代码
Der d;  // 构造时 Base() 里调的是 Base::init

⚠️ 为什么这么设计? 构造基类时,子类部分还没构造 ,此时调子类的虚函数会用到未初始化的成员------很危险。所以语言规定:构造/析构期间的虚函数调用,退化为当前类的普通调用。理解了这层"保护",就不会再踩这个坑。


五、纯虚函数与抽象类

❓ 什么是纯虚函数?抽象类有什么用?

纯虚函数 是只有声明、没有定义的虚函数(= 0)。含有纯虚函数的类叫抽象类,不能实例化,只能作为接口被继承。

cpp 复制代码
// 抽象类:接口
struct Shape {
    // 纯虚函数:必须由子类实现
    virtual double area() const = 0;
    virtual ~Shape() = default;
};
cpp 复制代码
// 子类必须实现 area,否则仍是抽象类
struct Circle : Shape {
    double r;
    double area() const override {
        return 3.14 * r * r;
    }
};
// Shape s;   // ❌ 抽象类不能实例化
Circle c{2.0};
Shape* p = &c;
std::cout << p->area();  // 12.56

💡 加分点

  • 纯虚函数也可以有定义 (在类外写 Shape::area() {...}),子类可显式调用------用于"提供默认实现但仍强制子类 override";
  • 抽象类是 C++ 实现接口编程的标准手段:基类只定契约,子类提供实现。

六、虚函数的代价

❓ 用虚函数有什么开销?

✅ 三方面代价:

  1. 运行期开销 :每次虚函数调用要查虚表 + 间接跳转,比普通调用慢(且阻碍内联);
  2. 空间开销 :每个对象多一个 vptr (通常 8 字节),每个类多一张虚表
  3. 缓存影响:间接跳转破坏指令缓存局部性。

🎯 实践建议 :虚函数的开销在绝大多数业务代码里可以忽略 ------一次查表就几纳秒。只有性能敏感的热路径 (如每帧百万次调用)才需要计较,那时考虑用模板/CRTP 做静态多态替代。别为了"怕慢"就不用虚函数,那是过早优化。


七、核心规则速查表

维度 规则
多态实现 虚函数 + 虚表 + vptr
虚表生成 编译期,每个类一张
vptr 设置 构造函数里,逐层更新
构造/析构里调虚函数 退化为当前类版本,不多态
纯虚函数 = 0,使类变抽象,不可实例化
开销 查表+间接调用,每对象多一 vptr

八、面试高频追问

❓ Q1:基类析构函数为什么要是虚函数?

✅ 当你用基类指针 delete 一个子类对象时,若析构不是虚函数,只会调基类析构,子类析构不执行 ------子类资源泄漏。这是未定义行为的高发区。所以"有虚函数的类,析构几乎一定要虚"。

❓ Q2:虚函数能内联吗?

✅ 语法上能加 inline,但通常不会被内联 ------虚函数要查虚表,编译期定不下来。例外:编译期能确定实际类型时(直接通过对象而非指针/引用调用),编译器可能内联掉。

❓ Q3:虚函数表存在哪?

✅ 每个类一张 ,是编译期生成的静态只读数据 (通常在 .rodata 段)。所有该类对象共享这一张虚表,只是各自的对象里有一个 vptr 指向它。

❓ Q4:构造函数能是虚函数吗?

不能。虚函数依赖 vptr,而 vptr 要在构造函数里设置------构造还没开始,vptr 还没就位,逻辑上矛盾。所以语言直接禁止虚构造函数。

❓ Q5:override 和 virtual 写哪个?

✅ 派生类重写虚函数,优先写 override(让编译器检查签名)。virtual 性质会沿继承链自动传递,派生类不写 virtual 也是虚函数;但 override 能挡住"签名写错导致没重写"的 bug(见关键字篇 final/override)。


九、总结速查表

概念 一句话
虚函数 运行期决定的成员函数
虚表 vtable 每类一张,存虚函数地址
vptr 每对象一个,指向所属类虚表
抽象类 含纯虚函数,不可实例化
多态实现 通过 vptr 查 vtable

一句话回顾

虚函数的本质是用虚表和 vptr 实现"运行期决定调用"。记住"每类一张虚表、每对象一个 vptr、构造析构里调虚函数会退化、基类析构要虚",这题就稳了。

如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们聊聊运算符重载,敬请关注 👋

相关推荐
chuan.bai1 小时前
Java RAG 实战(第 8 篇):Spring Boot RAG 查询 API
java·spring boot·贪心算法
FlyWIHTSKY1 小时前
idea中集成claude功能
java·ide·人工智能·intellij-idea·cloudera
Java成神之路-1 小时前
Spring AI 统一结构化返回:ChatModel / ChatClient 两种实现方式
java·springaialibaba
Mr. zhihao1 小时前
深度解析:为什么Java序列化需要搭配ByteArrayOutputStream?IO装饰器模式的精妙设计
java·开发语言·装饰器模式
存在morning1 小时前
【Python 开发实践 一】Python vs Go vs Java 三门语言对比
java·python·golang
vx-程序开发1 小时前
springboot旅游推介平台---附源码24175
java·spring boot·python·spring cloud·eclipse·django·idea
djjjx.2 小时前
【 C++ 】多态
开发语言·c++·多态
艾莉丝努力练剑2 小时前
【QT:解决问题】Qt5Core.dll:无法定位程序输入点
java·开发语言·qt·学习·面试
小蒜学长2 小时前
“喵汪联盟”宠物领养系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·宠物