C++ 多肽经典面试题

一、什么是多态?(必问)

👉 多态是:

同一接口,不同实现

在 C++ 中主要有两种:

1️⃣ 编译时多态(静态多态)

  • 函数重载(overload)
  • 模板(template)

2️⃣ 运行时多态(动态多态)⭐重点

依赖:

  • 虚函数(virtual)
  • 继承
  • 指针/引用调用

二、经典面试题

1、虚函数实现原理

问:虚函数是如何实现多态的?

👉核心答案:

通过 虚函数表(vtable)+ 虚指针(vptr)

内存模型:

每个带虚函数的类:

cpp 复制代码
class Base {
public:
    virtual void func() {}
};

对象内部:

cpp 复制代码
对象
 ├── vptr → 指向虚函数表

虚函数表:

cpp 复制代码
vtable:
  func → Base::func

派生类:

cpp 复制代码
vtable:
  func → Derived::func

调用时:

cpp 复制代码
Base* p = new Derived();
p->func();  // 动态绑定

实际执行:

cpp 复制代码
通过 vptr 找到 Derived::func()

2、为什么需要 virtual

如果不加 virtual:

cpp 复制代码
Base* p = new Derived();
p->func();  // 调用 Base 版本

加 virtual:

cpp 复制代码
virtual void func();

运行时绑定(Run-time binding)

3、虚析构函数

1、为什么基类析构函数要是虚函数?

错误写法:
cpp 复制代码
class Base {
public:
    ~Base() {}
};

调用:

cpp 复制代码
Base* p = new Derived();
delete p;  // ❌ 未调用 Derived 析构
正确写法:
cpp 复制代码
class Base {
public:
    virtual ~Base() {}
};

调用:

结果:先调用自己子类的析构函数,然后再调用父类的析构函数

cpp 复制代码
先调用 Derived::~Derived()
再调用 Base::~Base()

4、纯虚函数 vs 抽象类

纯虚函数:

cpp 复制代码
virtual void func() = 0;

特点:

  • 不能实例化
  • 子类必须实现

抽象类:

cpp 复制代码
class Base {
public:
    virtual void func() = 0;
};

作用:

  • 定义接口
  • 实现多态

5:虚函数可以是私有的吗?

可以,但:

  • 可以在类内调用
  • 可以被派生类覆盖
  • 但通常通过基类接口调用

6:构造函数可以是虚函数吗?

👉 ❌ 不可以

原因:

  • 构造时对象还没完整建立
  • vptr 还没初始化

7:多态什么时候失效?

1、通过对象调用

cpp 复制代码
Derived d;
Base b = d;
b.func();  // ❌ 对象切片

2、构造函数中调用虚函数

cpp 复制代码
Base::Base() {
    func();  // ❌ 调用 Base 版本
}

8:多态 vs 函数重载

类型 多态 重载
时间 运行时 编译时
机制 虚函数 函数签名
是否继承

9、override 和 final

override(推荐)

cpp 复制代码
void func() override;

作用:

  • 防止函数签名写错

final

cpp 复制代码
class Base final {};

cpp 复制代码
void func() final;

作用:

  • 阻止继承 / 重写

10、多态的本质(深度理解)

多态的本质:

"运行时根据对象类型选择函数"

技术实现:

指针 → vtable → 函数地址

11、虚函数底层原理

当我们写:

Base* p = new Derived();

p->func();

对象内存结构

cpp 复制代码
对象里会多一个隐藏指针:

[ vptr ] → 虚函数表(vtable)
[ 成员变量 ]

虚函数表(vtable)

cpp 复制代码
Derived::vtable:
    func → &Derived::func

调用过程(关键)

cpp 复制代码
p->func();

实际执行:

  1. 取 p 指向对象的 vptr

  2. 通过 vptr 找到 vtable

  3. 找到 func 的函数地址

  4. 间接调用该函数

汇编本质(伪):

mov rax, p ; 取对象地址

mov rax, rax ; 取 vptr

mov rax, rax+offset ; 找 func

call rax ; 间接调用


关键理解

普通函数调用:直接跳转(快)

虚函数调用:多了一层"查表"(慢一点)

12、为什么虚函数比普通函数慢?

核心原因:

1️⃣ 多一次间接寻址

普通函数:

call func

虚函数:

call vtable + offset


2️⃣ CPU Cache 不友好

虚函数:

函数地址是"运行时才知道"

👉 可能导致:

  • 指令预测失败
  • cache miss

3️⃣ 无法内联(inline)

编译器无法提前确定调用对象:

p->func(); // ❌ 不能 inline

👉 影响性能


🔥 一句话总结性能问题:

虚函数 = 动态分发 → 慢于静态绑定

13、模板 vs 虚函数(面试超级高频)

虚函数(运行时多态)

cpp 复制代码
class Base {
public:
    virtual void func() {}
};

特点:

运行时决定
有 vtable
有开销

模板(编译时多态)

cpp 复制代码
template<typename T>
void func(T obj) {
obj.func();
}

👉 编译器会:

为每个类型生成一份代码
维度 虚函数 模板
多态类型 运行时多态 编译时多态
性能 有开销(vtable) 接近0开销(可内联)
灵活性 高(运行时切换) 低(编译期确定)
代码体积 稳定 可能膨胀
使用场景 接口/框架 高性能计算

虚函数适合运行时多态,支持统一接口;

模板适合编译时多态,性能更高但会产生代码膨胀。

那个更好?

虚函数和模板没有绝对优劣,取决于使用场景:

  • 如果需要运行时动态选择行为(如插件、接口、多态容器),使用虚函数;

  • 如果追求性能、类型在编译期已确定,使用模板(或CRTP)更优。

虚函数用于运行时多态,适合需要动态行为切换的场景;

模板用于编译时多态,适合高性能和类型已知的场景。

在性能敏感的系统中,通常优先选择模板或CRTP来避免虚函数的开销。

虚函数

运行时决定调用哪个函数

本质:

动态绑定(runtime polymorphism)


模板

编译时生成具体代码

本质:

静态多态(compile-time polymorphism)

14、为什么很多高性能系统不用虚函数?

在这些领域:

  • 游戏引擎
  • 自动驾驶(感知模块)
  • 图像处理(你这个方向)
  • 高频交易

原因:

1️⃣ 追求极致性能

虚函数:

额外一次间接调用


2️⃣ 避免 cache miss

高性能系统更喜欢:

连续内存 + 可预测执行


3️⃣ 使用模板 / CRTP

cpp 复制代码
template<typename Derived>
class Base {
void interface() {
static_cast<Derived*>(this)->impl();
}
};

👉 这叫:

CRTP(静态多态)


15、CRTP(高级面试加分点)

cpp 复制代码
template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->impl();
    }
};

class Derived : public Base<Derived> {
public:
    void impl() {
        cout << "Derived" << endl;
    }
};

编译器在编译期就确定调用

说明
无虚函数
无运行时开销
可内联
编译期绑定

优点:

  • 🚀 零运行时开销
  • 🚀 可内联
  • 🚀 无 vtable

缺点:

  • 类型必须在编译期确定
  • 写法复杂
  • 不适合动态扩展

16、虚函数表在内存中到底长什么样?

cpp 复制代码
对象:
[ vptr ]
[ data1 ]
[ data2 ]

vtable:
[ func1地址 ]
[ func2地址 ]

多继承会怎样?

cpp 复制代码
class A { virtual void f(); };
class B { virtual void g(); };
class C : public A, public B {};

C 对象:

cpp 复制代码
[ A 的 vptr ]
[ B 的 vptr ]

👉 这会导致:

  • 多个 vptr
  • 复杂地址计算

17、虚函数的"隐藏成本"(面试很加分)

1️⃣ 对象体积变大

每个对象多一个 vptr


2️⃣ 破坏内存布局连续性

影响:

  • cache
  • SIMD
  • vectorization

3️⃣ 难以优化

编译器:

无法确定调用目标

18、虚函数 vs CRTP vs std::function

方式 本质 时间 性能 灵活性
虚函数 运行时多态 运行时
CRTP 编译时多态 编译时 高(0开销)
std::function 函数对象封装 运行时 较低 最高
方式 调用路径 能否内联
虚函数 vtable
CRTP 直接调用
std::function 间接调用

场景使用:

1️⃣ 虚函数适合

框架 / 插件 / 动态行为

例如:

  • Qt
  • 游戏引擎
  • UI系统

2️⃣ CRTP适合

高性能算法 / 底层库

例如:

  • 3D视觉
  • SLAM
  • 点云处理

3️⃣ std::function适合

业务层 / 回调 / 异步

例如:

  • 事件驱动
  • 多线程任务

19、为什么不用虚函数,而用 CRTP?

CRTP 是编译期多态,不需要虚函数表,
可以避免运行时开销,同时支持内联优化,
在高性能场景(如算法、图像处理)中更优。

**20、**为什么要"尽量避免虚函数"?

虚函数的问题不是"不能用",而是:

1. 运行时查表(vtable)
2. 破坏内联
3. cache 不友好
4. 不利于SIMD/批处理优化

这些是高频计算场景

性能 > 灵活性

替代方案:

方案 本质 性能 适用
模板 编译期多态 ⭐⭐⭐⭐⭐ 核心计算
CRTP 模板封装 ⭐⭐⭐⭐⭐ 库设计
std::function 函数封装 ⭐⭐⭐ 回调

方案1:模板(最常用)⭐⭐⭐⭐⭐

1️⃣ 传统虚函数写法
cpp 复制代码
class Filter {
public:
virtual void process(std::vector<float>& data) = 0;
};
2️⃣ 模板版本(推荐)
cpp 复制代码
template<typename T>
void process(std::vector<float>& data, T& algo) {
algo.process(data);
}

使用:

cpp 复制代码
class GaussianFilter {
public:
    void process(std::vector<float>& data) {
        // 处理逻辑
    }
};

GaussianFilter f;
process(data, f);

优势

✔ 编译期绑定

✔ 可内联

✔ 无 vtable

✔ 支持优化(SIMD)

方案2:CRTP(更底层)

cpp 复制代码
template<typename Derived>
class FilterBase {
public:
void process(std::vector<float>& data) {
static_cast<Derived*>(this)->processImpl(data);
}
};


class GaussianFilter : public FilterBase<GaussianFilter> {
public:
void processImpl(std::vector<float>& data) {
// 实现
}
};

使用:

cpp 复制代码
GaussianFilter f;
f.process(data);
🔥 本质

编译期已经确定调用函数

👉 等价:

直接调用 GaussianFilter::processImpl


🔥 优势
  • 零开销
  • 完全内联
  • 适合高性能库

SIMD + 模板(顶级优化)

cpp 复制代码
template<typename T>
void process_simd(T* data, int n) {
    for (int i = 0; i < n; i += 8) {
        // SIMD处理
    }
}

优势:

✔ 向量化
✔ cache友好
✔ 高吞吐

21、重写和重的区别

**重写是子类对父类虚函数的重新实现,属于运行时多态;**重载 → API设计问题(接口设计)
**重载是在同一作用域中函数名相同但参数不同,属于编译时多态。**重写 → 架构设计问题(多态与扩展)

重写用于改变行为,重载用于扩展接口。

重写(Override)

子类重新实现父类的虚函数

cpp 复制代码
class Base {
public:
    virtual void func() {
        cout << "Base" << endl;
    }
};

class Derived : public Base {
public:
    void func() override {   // 👈 重写
        cout << "Derived" << endl;
    }
};
特点:
  • 必须是 虚函数(virtual)
  • 函数签名必须一致
  • 运行时多态(动态绑定)
  • override 关键字(推荐)

重载(Overload)

同一个作用域内,函数名相同,但参数不同

cpp 复制代码
void func(int x) {}
void func(double x) {}   // 👈 重载
void func(int x, int y) {} // 👈 重载
特点:
  • 同一个函数名
  • 参数不同(个数 / 类型)
  • 编译期决定调用(静态绑定)
  • 与继承无关
对比项 重写 Override 重载 Overload
发生位置 子类 vs 父类 同一个类
是否需要继承 ✅ 需要 ❌ 不需要
是否需要 virtual ✅ 需要 ❌ 不需要
函数签名 必须相同 必须不同
绑定方式 运行时(动态) 编译时(静态)
作用 多态 接口扩展

22、什么是内联,优缺点

内联函数是指在调用处直接展开函数代码,而不是进行函数调用

类似宏,但更安全:

  • 有类型检查
  • 有作用域

普通函数

内联函数

优点

1️⃣ 消除函数调用开销

普通函数调用成本:

压栈 → 跳转 → 返回

内联:

直接执行

👉 高频函数(比如循环中)收益明显


2️⃣ 有利于编译器优化

内联后:

代码暴露给编译器

可以做:

  • 常量折叠
  • 死代码消除
  • 循环展开
  • 向量化(SIMD)

3️⃣ 类型安全(比宏好)
cpp 复制代码
#define ADD(a,b) a+b // ❌ 有坑

inline int add(int a,int b) {} // ✅ 安全

缺点

1️⃣ 代码膨胀(非常关键)

每次调用 → 展开一份代码

👉 如果函数很大:

可执行文件变大


2️⃣ 可能导致 cache 变差

代码变多:

指令缓存(I-cache)压力变大

👉 反而变慢(高级点)


3️⃣ 不是一定会内联(重点)

inline 只是"建议",不是强制

编译器可能拒绝:

  • 函数太大
  • 有循环
  • 有递归
  • 有虚函数

4️⃣ 调试困难

没有函数调用栈

22、什么时候不会内联

1、虚函数(默认)

cpp 复制代码
virtual void func();

因为在调用的时候才知道调用的是那个函数

2、递归函数

cpp 复制代码
int f(int n) {
    return f(n-1);
}

3、编译器觉得"展开不划算"

对比 inline
类型检查
作用域
调试 较好
安全性
相关推荐
Dovis(誓平步青云)3 小时前
《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》
c语言·开发语言·汇编·qt
code monkey.3 小时前
【Linux之旅】Linux 应用层自定义协议与序列化:从粘包问题到网络计算器
linux·网络·c++
草莓熊Lotso3 小时前
【Linux网络】深入理解 HTTP 协议(二):从协议格式到手写工业级 HTTP 服务器
linux·运维·服务器·网络·c++·http
AI人工智能+电脑小能手10 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
isyangli_blog12 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb20081112 小时前
FastAPI APIRouter
开发语言·python
Benszen12 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆12 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木12 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
Cosolar12 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构