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
类型检查
作用域
调试 较好
安全性
相关推荐
软件测试媛2 小时前
2026软件测试面试题大全(含答案+文档)
功能测试·测试工具·面试·ai软件测试
csdn_aspnet3 小时前
C# 求n边凸多边形的对角线数量(Find number of diagonals in n sided convex polygon)
开发语言·算法·c#
qq_254674413 小时前
Docker 中的 镜像(
开发语言
码云社区3 小时前
JAVA二手车交易二手车市场系统源码支持微信小程序+微信公众号+H5+APP
java·开发语言·微信小程序·二手交易·闲置回收
crescent_悦3 小时前
C++:The Largest Generation
java·开发语言·c++
paeamecium4 小时前
【PAT甲级真题】- Student List for Course (25)
数据结构·c++·算法·list·pat考试
NAGNIP4 小时前
一文搞懂CNN经典架构-DenseNet!
算法·面试
花间相见4 小时前
【MySQL面试题】—— MySQL面试高频问题汇总:从原理到实战,覆盖90%考点
数据库·mysql·面试
Lyyaoo.4 小时前
【JAVA基础面经】抽象类/方法与接口
java·开发语言