C++ 面试题大全(2025-2026 最新版)
涵盖 C++98 到 C++23,从基础到架构师级别,按主题分类整理。
目录
- [基础必考篇(TOP 10)](#基础必考篇(TOP 10))
- 面向对象:虚函数与多态
- [现代 C++ 新特性(C++11/14/17/20/23)](#现代 C++ 新特性(C++11/14/17/20/23))
- 智能指针
- 移动语义与完美转发
- [STL 容器底层原理](#STL 容器底层原理)
- 并发编程与无锁编程
- 设计模式
- 高频手撕代码题
- 大厂面试差异
- 分岗位高频题
- [Lambda 表达式深入](#Lambda 表达式深入)
- 操作系统基础
- [IO 多路复用](#IO 多路复用)
- 平台与架构
- 调试与版本控制
一、基础必考篇
Q1: 指针与引用的区别
| 特性 |
指针 |
引用 |
| 是否可为空 |
可以为 nullptr |
不可为空 |
| 可否重新绑定 |
可以指向不同对象 |
一旦绑定不可更改 |
| 是否需要解引用 |
需要 * 或 -> |
直接使用 |
| 是否占用内存 |
占用(存地址) |
不占(只是别名,编译器优化) |
| 可否多级 |
二级/多级指针 |
没有多级引用 |
| 底层实现 |
存地址 |
本质也是存地址(编译器视角) |
Q2: new/delete vs malloc/free
| 特性 |
new/delete |
malloc/free |
| 本质 |
C++ 运算符 |
C 库函数 |
| 调用构造函数 |
✅ 自动调用 |
❌ 不调用 |
| 调用析构函数 |
✅ 自动调用 |
❌ 不调用 |
| 失败行为 |
抛 std::bad_alloc 异常 |
返回 NULL |
| 申请大小 |
编译器自动计算 |
需手动计算字节数 |
| 重载 |
可重载 |
不可重载 |
| 内存来源 |
自由存储区 |
堆 |
Q3: const 关键字的多种用法
const int a = 10; // 1. 常量:值不可改
int const b = 20; // 同上,等价写法
const int* p1 = &a; // 2. 指向常量的指针:*p1不可改,p1可改
int const* p2 = &a; // 同上,等价写法
int* const p3 = &a; // 3. 常量指针:p3不可改,*p3可改
const int* const p4 = &a; // 4. 常量指针指向常量:都不能改
void foo() const; // 5. const成员函数:不能修改成员变量(mutable除外)
const std::string& bar(); // 6. const返回值:防止调用者修改返回值
Q4: static 关键字的作用
| 作用域 |
效果 |
| 局部静态变量 |
生命周期为整个程序,首次执行到定义处初始化(C++11起线程安全) |
| 全局静态变量/函数 |
作用域限制在当前文件(内部链接),外部文件不可见 |
| 静态成员变量 |
属于类而非对象,所有对象共享,需在类外定义 |
| 静态成员函数 |
无 this 指针,只能访问静态成员 |
Q5: 堆与栈的区别
| 特性 |
栈 (Stack) |
堆 (Heap) |
| 分配方式 |
编译器自动 |
程序员手动(new/malloc) |
| 大小限制 |
较小(通常几MB) |
较大(受系统内存限制) |
| 分配速度 |
极快(一条CPU指令) |
较慢(需查找空闲块) |
| 生命周期 |
作用域结束自动释放 |
需手动释放 |
| 碎片问题 |
无 |
可能产生 |
| 线程安全 |
天然线程安全 |
需同步 |
Q6: 深拷贝与浅拷贝
class String {
char* data;
public:
// 浅拷贝(默认拷贝构造):只复制指针
// String(const String& s) = default; // data 指向同一块内存
// 深拷贝:复制指针指向的内容
String(const String& s) {
data = new char[strlen(s.data) + 1];
strcpy(data, s.data);
}
};
Q7: 四种强制类型转换
| 转换 |
用途 |
特点 |
static_cast |
相关类型转换、非多态父子转换 |
编译期检查 |
dynamic_cast |
多态父子转换(含运行时类型检查) |
运行时检查,失败返回nullptr/抛异常 |
const_cast |
移除/添加const属性 |
仅改const属性 |
reinterpret_cast |
任意类型间转换(二进制重解释) |
最危险,慎用 |
Q8: extern "C" 的作用
C++ 编译器会做名称修饰(name mangling),extern "C" 告诉编译器按 C 链接方式处理,避免名称修饰。用于 C/C++ 混合编程。
extern "C" {
void c_function(int a);
}
Q9: inline 函数的理解
编译器将函数体展开到调用处,减少函数调用开销。现代编译器会自行判断是否内联,inline 关键字只是建议。主要解决头文件中函数重复定义的问题(内联函数允许多个编译单元内重复定义)。
Q10: struct 与 class 的区别
只有一点:默认访问权限不同 。struct 默认 public,class 默认 private。其他完全相同。
二、面向对象:虚函数与多态
多态实现原理:vtable + vptr
对象内存布局:
┌───────────────┐
│ vptr │──→ vtable(虚函数表,代码段/只读数据段)
├───────────────┤ ┌──────────────────────┐
│ 成员变量 │ │ &ClassName::func1 │
└───────────────┘ │ &ClassName::func2 │
└──────────────────────┘
| 概念 |
说明 |
| vtable(虚函数表) |
编译期生成,类级别,同类型对象共享,存于代码段 |
| vptr(虚表指针) |
对象级别,在构造函数中初始化 |
| 动态绑定流程 |
对象 → vptr → vtable索引 → 函数地址 → 调用 |
Q: 为什么构造函数不能是虚函数?
对象中的 vptr 是在构造函数执行期间才初始化的。如果构造函数是虚函数,调用前需要通过 vptr 查找,但此时 vptr 还未初始化------鸡生蛋问题。
Q: 为什么析构函数建议为虚函数?
当通过基类指针 delete 子类对象时,如果基类析构函数不是虚函数,则只会调用基类析构,子类资源泄漏。
Base* p = new Derived();
delete p; // ~Base() 非虚函数 → 只调 Base 析构 → 泄漏!
Q: 重载、重写、隐藏的区别
| 特性 |
重载 (Overload) |
重写/覆盖 (Override) |
隐藏 (Hide) |
| 作用域 |
同一类中 |
基类-派生类之间 |
基类-派生类之间 |
| 函数名 |
相同 |
相同 |
相同 |
| 参数 |
必须不同 |
必须相同 |
只需同名 |
| virtual |
不要求 |
必须 |
非虚函数 |
Q: 什么是对象切片(Object Slicing)?
子类对象按值传递给父类参数时,子类特有部分被"切掉",vptr 也变为父类的 vptr,多态消失。
Q: override 和 final 的作用
override:显式声明重写,编译器检查签名是否匹配
final:禁止子类进一步重写此函数,或禁止类被继承
三、现代 C++ 新特性
C++11 核心特性
| 特性 |
说明 |
auto / decltype |
类型自动推导 |
| 右值引用 && |
移动语义基础 |
std::move / std::forward |
移动 + 完美转发 |
| 智能指针 |
unique_ptr, shared_ptr, weak_ptr |
| Lambda 表达式 |
[capture](params) -> ret { body } |
nullptr |
类型安全的空指针 |
for 范围循环 |
for (auto& x : container) |
= delete / = default |
显式禁止/默认特殊函数 |
constexpr |
编译期常量表达式 |
| 线程库 |
std::thread, std::mutex, std::condition_variable |
C++14 增强
- 泛型 Lambda:
[](auto a, auto b) { return a + b; }
std::make_unique<T>() 工厂函数
constexpr 支持更多语法(循环、分支)
C++17 核心特性
| 特性 |
说明 |
用法 |
std::optional<T> |
可能无值的值类型 |
替代指针表示"可能为空" |
std::variant<T...> |
类型安全的联合体 |
替代 union |
std::string_view |
非拥有字符串视图 |
不拷贝字符串的高性能传递 |
if constexpr |
编译期条件分支 |
模板中根据类型走不同逻辑 |
| 结构化绑定 |
解构元组/结构体 |
auto [x, y, z] = tuple; |
std::any |
可存任意类型的容器 |
类型安全的 void* |
| CTAD |
类模板参数推导 |
std::pair p{1, 2.0}; 无需写模板参数 |
| Fold Expressions |
参数包折叠 |
(args + ...) |
// if constexpr 示例
template<typename T>
auto get_value(T t) {
if constexpr (std::is_pointer_v<T>)
return *t; // 编译期选此分支
else
return t; // 或此分支
}
C++20 核心特性
1. Concepts(概念)
编译期约束模板参数,让模板错误信息更清晰。
#include <concepts>
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template<Addable T>
T add(T a, T b) { return a + b; }
// 四种写法:
// 1. requires 子句
template<typename T> requires std::integral<T>
T f1(T a);
// 2. 约束模板参数
template<std::integral T>
T f2(T a);
// 3. 尾部 requires
template<typename T>
T f3(T a) requires std::integral<T>;
// 4. 缩写函数模板
std::integral auto f4(std::integral auto a);
2. Ranges(范围库)
管道式操作,惰性求值,零中间容器。
#include <ranges>
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 10; });
// result: 20, 40, 60(遍历时才计算,无临时容器)
std::ranges::sort(nums); // 直接传容器,无需 begin()/end()
| View Adaptor |
功能 |
views::filter(pred) |
过滤 |
views::transform(fn) |
映射 |
views::take(n) |
取前n个 |
views::drop(n) |
跳过前n个 |
views::reverse |
反向 |
views::iota |
生成序列 |
3. Coroutines(协程)
三个关键字:co_await、co_yield、co_return。无栈协程,挂起时状态存于堆分配的协程帧。
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
int tmp = a;
a = b;
b = tmp + b;
}
}
4. Modules(模块)
替代 #include 头文件机制,提升编译速度,隔离命名空间。
5. std::span<T>
轻量级视图,不拥有数据,安全替代 T* + size。
C++23 重点
| 特性 |
说明 |
std::expected<T, E> |
返回值带错误信息(替代异常) |
std::flat_map / std::flat_set |
连续内存存储的有序容器 |
std::mdspan |
多维视图 |
std::generator |
同步协程生成器 |
std::ranges::to<> |
将 View 收集到容器 |
views::enumerate |
带索引遍历 |
四、智能指针
总览对比
| 特性 |
unique_ptr |
shared_ptr |
weak_ptr |
| 所有权 |
独占 |
共享 |
无(观察者) |
| 引用计数 |
无 |
有(use_count) |
只增 weak_count |
| 拷贝 |
禁止 |
允许 |
允许 |
| 移动 |
允许 |
允许 |
允许 |
| 大小 |
裸指针级别(不含删除器) |
2 个指针(对象+控制块) |
2 个指针(同上) |
| 内存开销 |
最小 |
额外控制块 |
同 shared_ptr |
| 典型场景 |
工厂函数、独占资源、PIMPL |
多处共享、异步任务 |
打破循环引用、观察者模式 |
一、std::unique_ptr --- 独占所有权
核心语义 :同一时刻只有一个 unique_ptr 拥有对象。禁止拷贝,只能移动。
创建方式
#include <memory>
// 1. make_unique(C++14,推荐方式)
auto p1 = std::make_unique<int>(42); // 类型自动推导
auto p2 = std::make_unique<std::string>(10, 'x'); // 对应 string(10, 'x')
// 2. 从裸指针构造(不推荐直接写 new)
std::unique_ptr<int> p3(new int(100));
// 3. C++11 无 make_unique 时的写法
std::unique_ptr<int> p4(new int(200));
// 4. 数组(C++14 起专用重载)
auto arr = std::make_unique<int[]>(10); // int[10]
arr[0] = 1; // 支持 operator[]
所有权转移 --- 只能移动
auto p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // ❌ 编译错误!拷贝构造已删除
std::unique_ptr<int> p2 = std::move(p1); // ✅ 移动构造
// p1 == nullptr,所有权转移给了 p2
// 作为函数参数和返回值
std::unique_ptr<int> process(std::unique_ptr<int> input) {
*input += 1;
return input; // 编译器自动 move(RVO + 移动语义)
}
auto result = process(std::make_unique<int>(10)); // 临时对象直接移动
常用操作
auto p = std::make_unique<int>(42);
// 解引用
int val = *p; // 获取值
std::string s = p->c_str(); // 对指向对象的成员访问
// 获取裸指针(不转移所有权)
int* raw = p.get(); // 获取裸指针,unique_ptr 仍拥有对象
// ⚠️ 不要 delete raw!也不要保存 raw 到另一个智能指针中
// 释放所有权(返回裸指针,unique_ptr 变为空)
int* raw2 = p.release(); // p 不再拥有对象,调用者负责 delete
delete raw2; // 必须手动释放
// 重置(销毁旧对象,可选接管新对象)
p.reset(); // 直接销毁,p == nullptr
p.reset(new int(100)); // 销毁旧对象,接管新对象
// 交换
auto p2 = std::make_unique<int>(200);
p.swap(p2); // 交换两个 unique_ptr 所管理的对象
// 判空
if (p) { /* 非空 */ }
if (!p) { /* 为空 */ }
if (p == nullptr) { /* 为空 */ }
自定义删除器
// 场景:FILE* 需要 fclose 而非 delete
auto file_deleter = [](FILE* f) {
if (f) fclose(f);
};
std::unique_ptr<FILE, decltype(file_deleter)> fp(fopen("test.txt", "r"), file_deleter);
// 离开作用域自动调用 fclose
// 场景:malloc 分配的内存需要 free
auto free_deleter = [](void* p) { std::free(p); };
std::unique_ptr<char, decltype(free_deleter)> buf(
static_cast<char*>(std::malloc(1024)), free_deleter
);
// 场景:管理第三方资源
struct Connection { void close(); };
auto conn_deleter = [](Connection* c) { c->close(); };
std::unique_ptr<Connection, decltype(conn_deleter)> conn(new Connection, conn_deleter);
实现原理(极简版)
template<typename T>
class SimpleUniquePtr {
public:
explicit SimpleUniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
~SimpleUniquePtr() { if (ptr_) delete ptr_; }
SimpleUniquePtr(const SimpleUniquePtr&) = delete; // 禁止拷贝
SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete; // 禁止拷贝赋值
SimpleUniquePtr(SimpleUniquePtr&& other) noexcept // 移动构造
: ptr_(other.ptr_) { other.ptr_ = nullptr; }
SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept { // 移动赋值
if (this != &other) {
if (ptr_) delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
explicit operator bool() const { return ptr_ != nullptr; }
T* release() { T* tmp = ptr_; ptr_ = nullptr; return tmp; }
void reset(T* ptr = nullptr) { if (ptr_) delete ptr_; ptr_ = ptr; }
private:
T* ptr_;
};
二、std::shared_ptr --- 共享所有权
核心语义 :多个 shared_ptr 可共享同一对象的所有权,通过引用计数 管理生命周期。最后一个 shared_ptr 销毁时释放对象。
内存模型
┌─────────────────┐ ┌─────────────┐ ┌──────────────────────┐
│ shared_ptr A │────→│ 控制块 │ │ 对象 T │
├─────────────────┤ ├─────────────┤ ├──────────────────────┤
│ ptr_ ──────────┼──→ │ use_count=2 │ │ ... 数据成员 ... │
└─────────────────┘ │ weak_count=0│ └──────────────────────┘
│ deleter │
┌─────────────────┐ │ allocator │
│ shared_ptr B │ └─────────────┘
├─────────────────┤ ↑
│ ptr_ ──────────┼───────────┘
│ ctrl_ ─────────┼──→ 控制块
└─────────────────┘
释放时机:
- use_count → 0:销毁对象 T(调用 deleter)
- use_count=0 且 weak_count=0:销毁控制块
创建方式
// 1. make_shared(强烈推荐)
auto sp1 = std::make_shared<int>(42);
auto sp2 = std::make_shared<std::string>("hello");
auto sp3 = std::make_shared<std::vector<int>>(100, 0); // vector(100, 0)
// 2. make_shared 用于数组(C++20)
auto arr = std::make_shared<int[]>(10);
// 3. 从裸指针构造(不推荐)
std::shared_ptr<int> sp4(new int(100)); // 两次内存分配
// 4. 从 unique_ptr 移动构造(转移所有权)
auto up = std::make_unique<int>(42);
std::shared_ptr<int> sp5 = std::move(up); // up == nullptr
拷贝与引用计数
auto sp1 = std::make_shared<int>(42);
std::cout << sp1.use_count() << "\n"; // 1
{
std::shared_ptr<int> sp2 = sp1; // 引用计数 +1
std::cout << sp1.use_count() << "\n"; // 2
std::shared_ptr<int> sp3(sp1); // +1
std::cout << sp1.use_count() << "\n"; // 3
auto sp4 = sp1; // +1
std::cout << sp1.use_count() << "\n"; // 4
} // sp2, sp3, sp4 析构,引用计数回到 1
std::cout << sp1.use_count() << "\n"; // 1
// sp1 析构 → use_count 变为 0 → 对象被释放
常用操作
auto sp = std::make_shared<int>(42);
// 访问对象
int val = *sp;
// sp->member; // 访问成员
// 获取裸指针
int* raw = sp.get();
// 重置
sp.reset(); // 释放当前对象,sp 变空(use_count 减 1)
sp.reset(new int(200)); // 释放当前对象,接管新对象
// 判空
if (sp) { /* 非空 */ }
if (sp == nullptr) { /* 为空 */ }
// 检查引用计数(主要用于调试)
long n = sp.use_count(); // 当前 shared_ptr 数量
bool uniq = sp.unique(); // use_count == 1?(C++20 起废弃)
自定义删除器
// 自定义删除器(与 unique_ptr 不同,删除器不改变 shared_ptr 类型)
auto deleter = [](int* p) { delete p; };
std::shared_ptr<int> sp1(new int(42), deleter);
std::shared_ptr<int> sp2(new int(100), [](int* p) { delete p; });
// make_shared 不支持自定义删除器,有需要时用 new + 删除器
// 但 make_shared 结合默认删除器在所有其他场景都是首选
线程安全
┌──────────────────────────────────────────────┐
│ shared_ptr 的线程安全性分两层: │
├──────────────────────────────────────────────┤
│ 1. 控制块/引用计数:线程安全(atomic 操作) │
│ → 多个线程拷贝/销毁同一个 shared_ptr 安全 │
│ 2. 管理的对象数据:不保证线程安全 │
│ → 多线程访问对象成员需要额外 mutex │
│ 3. 同一个 shared_ptr 对象:非线程安全 │
│ → 多线程同时赋值给同一个 shared_ptr 需同步 │
└──────────────────────────────────────────────┘
// ✅ 安全:不同线程持有各自的 shared_ptr 拷贝
void worker(std::shared_ptr<Data> sp) { /* 对象生命周期受保护 */ }
auto sp = std::make_shared<Data>();
std::thread t1(worker, sp); // 传值时拷贝,引用计数原子递增
std::thread t2(worker, sp); // 同上
// ❌ 不安全:多线程访问对象内部数据
std::mutex mtx;
void safe_worker(std::shared_ptr<Data> sp) {
std::lock_guard<std::mutex> lock(mtx);
sp->modify(); // 对象数据仍需 mutex 保护
}
enable_shared_from_this
问题 :在成员函数中如何正确返回 this 的 shared_ptr?
// ❌ 错误的做法
struct Bad {
std::shared_ptr<Bad> get_shared() {
return std::shared_ptr<Bad>(this); // 危险!创建新的控制块
}
};
// 调用两次 get_shared() 会创建两个控制块 → double free!
// ✅ 正确的做法
struct Good : public std::enable_shared_from_this<Good> {
std::shared_ptr<Good> get_shared() {
return shared_from_this(); // 复用已有的控制块,引用计数 +1
}
static std::shared_ptr<Good> create() {
return std::make_shared<Good>();
}
};
auto obj = Good::create(); // shared_ptr<Good>
auto obj2 = obj->get_shared(); // 共享同一个控制块,use_count = 2
重要约束 :调用 shared_from_this() 前,对象必须已被 shared_ptr 管理。通常将构造函数设为 private + 提供静态工厂方法。
make_shared vs new 深入
| 特性 |
new + shared_ptr |
make_shared |
| 内存分配 |
2 次(对象 + 控制块) |
1 次(合并分配) |
| 异常安全 |
f(shared_ptr<T>(new T), g()) 可能泄漏 |
完全安全 |
| 缓存局部性 |
差(两块内存分散) |
好(连续内存) |
| 自定义删除器 |
✅ 支持 |
❌ 不支持 |
| 内存释放延迟 |
无延迟 |
对象内存随控制块一起释放(weak_ptr 存活时) |
| 弱指针下的内存占用 |
只占用对象大小 |
占用对象+控制块大小(直到所有 weak_ptr 也释放) |
三、std::weak_ptr --- 观察者 / 弱引用
核心语义 :不拥有对象所有权,只是"观察" shared_ptr 管理的对象。weak_ptr 不影响引用计数中的 use_count,只增加 weak_count。
为什么需要 weak_ptr?
- 打破循环引用 :
shared_ptr 循环引用导致内存泄漏
- 缓存/观察者模式:知道对象可能已被销毁,需要先检查
- 安全的悬垂检查 :将
shared_ptr 赋值给 weak_ptr 后,即使原对象被释放也不会 crash
创建 weak_ptr
auto sp = std::make_shared<int>(42);
// 只能从 shared_ptr 或另一个 weak_ptr 创建
std::weak_ptr<int> wp1(sp); // 从 shared_ptr
std::weak_ptr<int> wp2(wp1); // 从另一个 weak_ptr(拷贝)
std::weak_ptr<int> wp3 = sp; // 隐式转换
// ⚠️ 不能直接从裸指针创建
// std::weak_ptr<int> wp(new int(42)); // ❌ 编译错误
核心方法
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;
// 1. expired() --- 检查对象是否已释放
if (!wp.expired()) {
// 对象仍然存活(非原子检查,存在 TOCTOU 问题)
}
// 2. lock() --- 尝试获取 shared_ptr(推荐方式)
if (auto locked = wp.lock()) {
// locked 是 shared_ptr<int>,确保对象在作用域内不会释放
*locked = 100;
// use_count 临时 +1,离开 if 块后 -1
} else {
// 对象已释放
}
// 3. use_count() --- 当前 shared_ptr 数量(主要用于调试)
long n = wp.use_count(); // 返回 1(sp 还在)
// 4. reset() --- 清空弱引用
wp.reset(); // wp 不再观察任何对象
打破循环引用(详细示例)
#include <memory>
#include <iostream>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> bptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> aptr; // ← 关键:用 weak_ptr 而非 shared_ptr
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->bptr = b; // A 持有 B 的 shared_ptr
b->aptr = a; // B 持有 A 的 weak_ptr(弱引用,不影响引用计数)
// 离开作用域:
// a 的 use_count = 1(仅 a 自己持有)→ 销毁 A
// A 析构 → bptr 释放 → b 的 use_count = 1 → 销毁 B
// ✅ 两个对象都正确释放!
}
内存泄漏版(全用 shared_ptr): 安全版(一端 weak_ptr):
A ←────── B A ←────── B
│ │ │ │
└─── shared ──→ B └─── shared ──→ B
shared ──┘ weak ──→ A (不影响计数)
use_count(A) = 2 ⇢ 永远不为 0 use_count(A) = 1 ⇢ 正确释放
use_count(B) = 1 use_count(B) = 1 ⇢ 正确释放
实用场景:观察者模式 + 自动清理
class Subject {
std::vector<std::weak_ptr<IObserver>> observers_;
public:
void attach(std::shared_ptr<IObserver> obs) {
observers_.push_back(obs); // 存 weak_ptr
}
void notify(const Event& ev) {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto obs = it->lock()) { // 尝试获取 shared_ptr
obs->onEvent(ev); // 观察者仍存活,通知
++it;
} else {
it = observers_.erase(it); // 观察者已销毁,自动清理
}
}
}
};
// 优点:观察者销毁后无需手动取消注册,Subject 自动跳过
实用场景:缓存
class ImageCache {
std::unordered_map<std::string, std::weak_ptr<Image>> cache_;
public:
std::shared_ptr<Image> get(const std::string& key) {
auto& wp = cache_[key];
if (auto img = wp.lock()) {
return img; // 缓存命中,图片仍在内存中
}
auto img = std::make_shared<Image>(loadFromDisk(key));
wp = img; // 更新缓存
return img;
}
// 当外部不再使用某张图片时,缓存项自动失效(weak_ptr expired)
// 不会因缓存持有 shared_ptr 而导致图片永远无法释放
};
四、三种智能指针选择指南
是否需要共享所有权?
/ \
否 是
/ \
┌──────────┐ ┌───────────────┐
│unique_ptr │ 是否可能产生循环引用? │
└──────────┘ / \
是 否
/ \
┌──────────────┐ ┌────────────┐
│shared_ptr │ │ shared_ptr │
│+ weak_ptr │ │ │
│(一端用weak) │ └────────────┘
└──────────────┘
核心使用原则
| 原则 |
说明 |
优先 unique_ptr |
90% 场景都用独占所有权即可 |
需要共享才用 shared_ptr |
引用计数有开销(atomic ±1) |
用 make_shared/make_unique |
异常安全 + 性能更好(一次分配) |
禁止裸 new |
C++14 起,new 只出现在工厂函数/make 函数内部 |
不要从同一个裸指针创建多个 shared_ptr |
导致多个控制块 → double free |
weak_ptr 不负责释放 |
weak_ptr 不能直接解引用,必须 lock() 先 |
继承 enable_shared_from_this |
需要从 this 安全获取 shared_ptr 时使用 |
五、移动语义与完美转发
值类别分类
| 类别 |
说明 |
例如 |
| 左值 (lvalue) |
有名字、可寻址、可重复访问 |
int a = 10; 中 a |
| 纯右值 (prvalue) |
临时对象、字面量 |
42、std::string("hello") |
| 将亡值 (xvalue) |
资源即将被转移 |
std::move(x) 的返回值 |
std::move vs std::forward
| 特性 |
std::move |
std::forward |
| 功能 |
无条件转为右值 |
有条件转发(保持原值类别) |
| 本质 |
static_cast<T&&> |
依赖引用折叠的智能转换 |
| 使用场景 |
资源所有权转移 |
模板中完美转发参数 |
| 参数推导 |
自动推导 |
必须显式指定模板参数 |
核心误区
std::move 不移动任何东西! 它只是一个无条件的类型转换,真正的移动发生在移动构造函数/移动赋值运算符中。
具名右值引用变量是左值! 任何有名字的变量都是左值,即使类型是右值引用。
void foo(std::string&& s) {
data = s; // ❌ s 是左值,调拷贝
data = std::move(s); // ✅ 强制转右值,调移动
}
引用折叠规则
| 组合 |
结果 |
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
规则:有一个是左值引用,结果就是左值引用;两个都是右值引用才是右值引用。
完美转发失败的情况
- 大括号初始化列表
{}(无法推导类型)
0 或 NULL 作为空指针常量
- 位域(bit-fields)
- 仅声明但未定义的
static const 成员
六、STL 容器底层原理
vector
| 项目 |
说明 |
| 底层结构 |
连续内存的动态数组 |
| 内部指针 |
start(首), finish(尾+1), end_of_storage(容量尾) |
| 扩容倍数 |
MSVC: 1.5倍, GCC: 2倍 |
| 随机访问 |
O(1) |
| 尾部插入 |
均摊 O(1),偶尔扩容时为 O(n) |
| 中间插入/删除 |
O(n) |
| 迭代器失效 |
扩容时全部失效;insert/erase 当前位置及之后失效 |
size() = finish - start // 元素个数
capacity() = end_of_storage - start // 已分配空间
扩容为何 1.5 倍而非 2 倍? 2 倍扩容后,释放的旧内存永远无法被新的扩容请求复用,产生碎片。1.5 倍则可累积复用。
map
| 项目 |
说明 |
| 底层结构 |
红黑树(自平衡二叉搜索树) |
| 插入/查找/删除 |
O(log n) |
| 元素顺序 |
按 key 自动排序 |
| 迭代器类型 |
双向迭代器 |
| 适用场景 |
需有序遍历、范围查询、性能稳定性 |
红黑树 vs AVL 树: 红黑树非严格平衡,插入删除旋转次数更少,综合场景性能更优。STL 因此选择红黑树。
unordered_map
| 项目 |
说明 |
| 底层结构 |
哈希表(开链法/拉链法) |
| bucket 结构 |
vector + 单向链表 |
| 查找 |
平均 O(1),最坏 O(n) |
| Rehash 时机 |
load_factor() > max_load_factor() 时 |
| 迭代器 |
单向迭代器(forward iterator) |
| key 要求 |
必须支持 std::hash<Key> 和 operator== |
负载因子 (load factor) : size() / bucket_count(),默认最大为 1.0。
map vs unordered_map 选择
| 场景 |
推荐 |
| 需要有序 |
map |
| 范围查询 |
map |
| 实时系统(稳定性能) |
map |
| 纯快速查找 |
unordered_map |
| 海量数据平均性能 |
unordered_map |
| 内存受限 |
map |
vector 的 resize() vs reserve()
| 特性 |
resize(n) |
reserve(n) |
| 修改 size |
✅ |
❌ |
| 新增元素 |
✅(默认值填充) |
❌ |
| 用途 |
改变元素个数 |
预留空间减少扩容 |
vector vs list 深入对比
| 特性 |
vector |
list |
| 底层结构 |
连续内存(动态数组) |
双向链表(非连续节点) |
| 随机访问 |
O(1) |
O(n) |
| 头部插入/删除 |
O(n) |
O(1) |
| 尾部插入/删除 |
均摊 O(1) |
O(1) |
| 中间插入/删除 |
O(n)(需移动元素) |
O(1)(已有迭代器定位) |
| 内存占用 |
紧凑,无额外指针开销 |
每节点额外 2 个指针(前驱+后继) |
| 缓存友好 |
✅ 极好(连续内存,预读命中) |
❌ 差(节点分散,cache miss 频繁) |
| 迭代器失效 |
扩容全失效;插入删除后部分失效 |
仅被删除节点失效,其余稳定 |
| 遍历性能 |
极高(指针自增,CPU 预取) |
较慢(指针跳转,每次可能 cache miss) |
选型原则:
- 默认选
vector------连续内存带来的缓存优势在绝大多数场景胜过链表
- 只有频繁在中间做 O(1) 插入删除且数据量大到移动成本不可接受时,才考虑
list
- 需要迭代器长期稳定(插入不导致失效)时用
list
手写队列(数组实现 + 链表实现)
// 数组实现(环形队列)
template<typename T>
class ArrayQueue {
public:
explicit ArrayQueue(size_t cap) : data_(cap), head_(0), tail_(0), size_(0), cap_(cap) {}
bool push(const T& val) {
if (size_ == cap_) return false; // 满
data_[tail_] = val;
tail_ = (tail_ + 1) % cap_;
size_++;
return true;
}
bool pop(T& val) {
if (size_ == 0) return false; // 空
val = data_[head_];
head_ = (head_ + 1) % cap_;
size_--;
return true;
}
bool empty() const { return size_ == 0; }
size_t size() const { return size_; }
private:
std::vector<T> data_;
size_t head_, tail_, size_, cap_;
};
// 链表实现
template<typename T>
class ListQueue {
public:
void push(const T& val) {
auto node = std::make_unique<Node>(val);
if (!tail_) {
head_ = std::move(node);
tail_ = head_.get();
} else {
tail_->next = std::move(node);
tail_ = tail_->next.get();
}
size_++;
}
bool pop(T& val) {
if (!head_) return false;
val = head_->data;
head_ = std::move(head_->next);
if (!head_) tail_ = nullptr;
size_--;
return true;
}
private:
struct Node {
T data;
std::unique_ptr<Node> next;
Node(const T& d) : data(d) {}
};
std::unique_ptr<Node> head_;
Node* tail_ = nullptr; // 裸指针观察者,不拥有所有权
size_t size_ = 0;
};
七、并发编程与无锁编程
std::atomic 六大内存序
| 内存序 |
含义 |
典型场景 |
memory_order_relaxed |
只保证原子性,无顺序约束 |
引用计数自增 |
memory_order_acquire |
之后的读写不能重排到此之前 |
消费者读取标志 |
memory_order_release |
之前的读写不能重排到此之后 |
生产者写入标志 |
memory_order_acq_rel |
兼具 acquire + release |
CAS 循环 |
memory_order_seq_cst |
全局顺序一致性(最强最慢) |
mutex 底层实现 |
memory_order_consume |
仅依赖排序(已基本废弃) |
几乎不用 |
volatile vs atomic
| 特性 |
volatile |
std::atomic |
| 保证原子性 |
❌ |
✅ |
| 防止编译器重排 |
❌(仅防寄存器缓存) |
✅ |
| 防止 CPU 乱序 |
❌ |
✅(生成内存屏障) |
| 用途 |
硬件寄存器、信号处理 |
多线程同步 |
release-acquire 同步(必考)
std::atomic<bool> ready{false};
int data = 0;
// 生产者
void producer() {
data = 42;
ready.store(true, std::memory_order_release); // 之前的所有写入对 acquire 可见
}
// 消费者
void consumer() {
while (!ready.load(std::memory_order_acquire)) // 与 release 配对
;
assert(data == 42); // 一定成功!
}
无锁自旋锁
class SpinLock {
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag.test_and_set(std::memory_order_acquire))
; // 自旋等待
}
void unlock() {
flag.clear(std::memory_order_release);
}
};
无锁栈 push(CAS)
template<typename T>
class LockFreeStack {
struct Node { T data; Node* next; Node(const T& d) : data(d), next(nullptr) {} };
std::atomic<Node*> head{nullptr};
public:
void push(const T& val) {
Node* node = new Node(val);
node->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(
node->next, node,
std::memory_order_release,
std::memory_order_relaxed))
; // CAS 失败则重试
}
};
compare_exchange_weak vs compare_exchange_strong
- weak:可能伪失败(spurious failure),性能更好,必须放在循环中
- strong:保证只有值不匹配时才失败,但某些平台有额外开销
x86 vs ARM 差异
| 平台 |
内存模型 |
acquire/release 开销 |
| x86 |
强内存模型 |
几乎零开销(只约束编译器) |
| ARM/RISC-V |
弱内存模型 |
生成 DMB/DSB 屏障指令,有真实开销 |
互斥锁(std::mutex)用法详解
#include <mutex>
// 1. 裸 mutex(不推荐直接用)
std::mutex mtx;
void bad_usage() {
mtx.lock();
// ... 若中间抛异常,锁永远不会释放!
mtx.unlock();
}
// 2. lock_guard:最简单 RAII 封装(不可手动解锁)
void good_with_guard() {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时解锁
// 临界区代码...
} // 出作用域自动解锁,异常安全
// 3. unique_lock:可延迟锁定、手动解锁、配合条件变量
void good_with_unique_lock() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁
// ... 做一些无需锁的操作 ...
lock.lock(); // 显式加锁
// 临界区...
lock.unlock(); // 提前解锁
// ... 无需锁的操作 ...
lock.lock(); // 再次加锁
} // 析构时自动解锁
// 4. scoped_lock (C++17):同时锁多个 mutex,避免死锁
std::mutex mtx1, mtx2;
void transfer() {
std::scoped_lock lock(mtx1, mtx2); // 原子地锁定两个 mutex
// 临界区...
}
| 互斥锁类型 |
特点 |
std::mutex |
基础互斥锁,不可递归 |
std::recursive_mutex |
同一线程可多次加锁 |
std::timed_mutex |
支持 try_lock_for() / try_lock_until() |
std::shared_mutex (C++17) |
读写锁:多读单写 |
std::lock_guard |
最简单 RAII,不可手动解锁 |
std::unique_lock |
灵活 RAII,可延迟/手动解锁/配合条件变量 |
std::scoped_lock (C++17) |
多锁 RAII,死锁避免 |
线程 vs 进程
| 特性 |
进程 (Process) |
线程 (Thread) |
| 定义 |
资源分配的基本单位 |
CPU 调度的基本单位 |
| 地址空间 |
独立,进程间默认隔离 |
共享,同一进程内线程共享地址空间 |
| 通信方式 |
管道、消息队列、共享内存、socket |
共享内存(需同步保护) |
| 创建开销 |
大(复制页表、分配资源) |
小(只需分配栈和寄存器) |
| 切换开销 |
大(切换页表、刷新 TLB) |
小(同进程只需切换寄存器) |
| 安全性 |
一个崩溃不影响其他进程 |
一个崩溃可能导致整个进程崩溃 |
| 数据共享 |
困难,需要 IPC |
容易,但需同步保护 |
线程池(Thread Pool)
为什么要用线程池? 线程创建/销毁有开销,频繁创建会降低性能。线程池预先创建一组工作线程,任务到来时分配给空闲线程执行,任务完成后线程不销毁而是等待新任务。
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
class ThreadPool {
public:
explicit ThreadPool(size_t num_threads) : stop_(false) {
for (size_t i = 0; i < num_threads; ++i) {
workers_.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mtx_);
// 等待任务或停止信号
condition_.wait(lock, [this] {
return stop_ || !tasks_.empty();
});
if (stop_ && tasks_.empty()) return;
task = std::move(tasks_.front());
tasks_.pop();
}
task(); // 执行任务(不在锁内)
}
});
}
}
// 提交任务,返回 future 以获取结果
template<typename F, typename... Args>
auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
using ReturnType = decltype(f(args...));
auto task = std::make_shared<std::packaged_task<ReturnType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<ReturnType> result = task->get_future();
{
std::lock_guard<std::mutex> lock(queue_mtx_);
if (stop_) throw std::runtime_error("submit on stopped ThreadPool");
tasks_.emplace([task] { (*task)(); });
}
condition_.notify_one();
return result;
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(queue_mtx_);
stop_ = true;
}
condition_.notify_all();
for (std::thread& worker : workers_) {
if (worker.joinable()) worker.join();
}
}
private:
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex queue_mtx_;
std::condition_variable condition_;
bool stop_;
};
八、设计模式
1. 单例模式(Singleton)
现代 C++ 推荐写法(C++11 局部静态变量线程安全):
class Singleton {
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全,Meyers Singleton
return instance;
}
private:
Singleton() = default;
};
饿汉 vs 懒汉
| 方式 |
初始化时机 |
线程安全 |
特点 |
| 饿汉 |
类加载时 |
✅ 天生安全 |
空间换时间 |
| 懒汉 |
首次调用时 |
需同步保护 |
时间换空间,延迟加载 |
2. 工厂模式(Factory)
三种工厂对比:
| 模式 |
说明 |
优点 |
缺点 |
| 简单工厂 |
一个工厂类按参数创建产品 |
简单 |
新增产品需改工厂类 |
| 工厂方法 |
定义创建接口,子类决定实例化 |
符合开闭原则 |
每增产品需增工厂类 |
| 抽象工厂 |
创建一系列相关对象 |
保证产品族一致性 |
扩展产品族困难 |
现代 C++ 可注册工厂:
class Factory {
public:
using Creator = std::function<std::unique_ptr<Product>()>;
void registerProduct(const std::string& type, Creator creator) {
creators_[type] = std::move(creator);
}
std::unique_ptr<Product> createProduct(const std::string& type) {
if (auto it = creators_.find(type); it != creators_.end())
return it->second();
throw std::runtime_error("Unknown type: " + type);
}
private:
std::unordered_map<std::string, Creator> creators_;
};
3. 观察者模式(Observer)
现代 C++ 实现(std::function + std::weak_ptr):
class Subject {
std::vector<std::weak_ptr<IObserver>> observers_;
public:
void attach(std::shared_ptr<IObserver> obs) {
observers_.push_back(obs);
}
void notify(const Data& data) {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto obs = it->lock()) {
obs->update(data);
++it;
} else {
it = observers_.erase(it); // 自动清理已销毁的观察者
}
}
}
};
SOLID 六大原则
| 原则 |
核心思想 |
| 单一职责(SRP) |
一个类只负责一项职责 |
| 开闭原则(OCP) |
对扩展开放,对修改封闭 |
| 里氏替换(LSP) |
子类可以替换父类出现 |
| 依赖倒置(DIP) |
依赖抽象而非具体实现 |
| 接口隔离(ISP) |
多个专用接口优于单一接口 |
| 迪米特法则(LoD) |
最少知道原则,降低耦合 |
九、高频手撕代码题
1. 手写 String 类(必考🔥)
class String {
public:
String(const char* str = "") {
if (str == nullptr) str = "";
len_ = strlen(str);
data_ = new char[len_ + 1];
strcpy(data_, str);
}
String(const String& other) { // 拷贝构造
len_ = other.len_;
data_ = new char[len_ + 1];
strcpy(data_, other.data_);
}
String(String&& other) noexcept { // 移动构造
len_ = other.len_;
data_ = other.data_;
other.data_ = nullptr;
other.len_ = 0;
}
String& operator=(const String& other) { // 拷贝赋值
if (this != &other) {
delete[] data_;
len_ = other.len_;
data_ = new char[len_ + 1];
strcpy(data_, other.data_);
}
return *this;
}
String& operator=(String&& other) noexcept { // 移动赋值
if (this != &other) {
delete[] data_;
len_ = other.len_;
data_ = other.data_;
other.data_ = nullptr;
other.len_ = 0;
}
return *this;
}
~String() { delete[] data_; }
private:
char* data_;
size_t len_;
};
2. 手写简化版 shared_ptr
template<typename T>
class SimpleSharedPtr {
public:
explicit SimpleSharedPtr(T* ptr = nullptr)
: ptr_(ptr), ref_count_(ptr ? new int(1) : nullptr) {}
SimpleSharedPtr(const SimpleSharedPtr& other)
: ptr_(other.ptr_), ref_count_(other.ref_count_) {
if (ref_count_) (*ref_count_)++;
}
SimpleSharedPtr& operator=(const SimpleSharedPtr& other) {
if (this != &other) {
release();
ptr_ = other.ptr_;
ref_count_ = other.ref_count_;
if (ref_count_) (*ref_count_)++;
}
return *this;
}
~SimpleSharedPtr() { release(); }
T* operator->() const { return ptr_; }
T& operator*() const { return *ptr_; }
int use_count() const { return ref_count_ ? *ref_count_ : 0; }
private:
void release() {
if (ref_count_ && --(*ref_count_) == 0) {
delete ptr_;
delete ref_count_;
}
ptr_ = nullptr;
ref_count_ = nullptr;
}
T* ptr_;
int* ref_count_;
};
3. 线程安全单例(Meyers Singleton)
class Singleton {
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;
};
4. 生产者-消费者模型
#include <mutex>
#include <condition_variable>
#include <queue>
template<typename T>
class ProducerConsumer {
public:
void produce(T item) {
std::unique_lock<std::mutex> lock(mtx_);
not_full_.wait(lock, [this] { return queue_.size() < capacity_; });
queue_.push(std::move(item));
not_empty_.notify_one();
}
T consume() {
std::unique_lock<std::mutex> lock(mtx_);
not_empty_.wait(lock, [this] { return !queue_.empty(); });
T item = std::move(queue_.front());
queue_.pop();
not_full_.notify_one();
return item;
}
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable not_empty_;
std::condition_variable not_full_;
size_t capacity_ = 100;
};
5. 线程安全 LRU 缓存(O(1) get/put)
class LRUCache {
public:
LRUCache(int capacity) : capacity_(capacity) {}
int get(int key) {
std::lock_guard<std::mutex> lock(mtx_);
auto it = map_.find(key);
if (it == map_.end()) return -1;
// 移到链表头部(最近使用)
list_.splice(list_.begin(), list_, it->second);
return it->second->second;
}
void put(int key, int value) {
std::lock_guard<std::mutex> lock(mtx_);
auto it = map_.find(key);
if (it != map_.end()) {
it->second->second = value;
list_.splice(list_.begin(), list_, it->second);
return;
}
if (list_.size() >= capacity_) {
int old_key = list_.back().first;
list_.pop_back();
map_.erase(old_key);
}
list_.emplace_front(key, value);
map_[key] = list_.begin();
}
private:
int capacity_;
std::list<std::pair<int, int>> list_;
std::unordered_map<int, std::list<std::pair<int, int>>::iterator> map_;
std::mutex mtx_;
};
6. 线程安全环形缓冲区(Ring Buffer)
template<typename T>
class RingBuffer {
public:
explicit RingBuffer(size_t size)
: buffer_(size), head_(0), tail_(0), count_(0) {}
bool push(const T& item) {
if (count_ == buffer_.size()) return false; // 满
buffer_[head_] = item;
head_ = (head_ + 1) % buffer_.size();
count_++;
return true;
}
bool pop(T& item) {
if (count_ == 0) return false; // 空
item = buffer_[tail_];
tail_ = (tail_ + 1) % buffer_.size();
count_--;
return true;
}
private:
std::vector<T> buffer_;
std::atomic<size_t> head_;
std::atomic<size_t> tail_;
std::atomic<size_t> count_;
};
7. 内存池(简易版本)
class MemoryPool {
public:
MemoryPool(size_t block_size, size_t block_count)
: block_size_(block_size) {
pool_ = ::operator new(block_size * block_count);
for (size_t i = 0; i < block_count; ++i) {
void* block = static_cast<char*>(pool_) + i * block_size;
free_list_.push_back(block);
}
}
void* allocate() {
if (free_list_.empty()) throw std::bad_alloc();
void* ptr = free_list_.back();
free_list_.pop_back();
return ptr;
}
void deallocate(void* ptr) {
free_list_.push_back(ptr);
}
~MemoryPool() { ::operator delete(pool_); }
private:
void* pool_;
size_t block_size_;
std::vector<void*> free_list_;
};
更多手写题清单
| 序号 |
题目 |
难度 |
考察点 |
| 1 |
手写 String 类 |
⭐⭐⭐ |
拷贝/移动构造、赋值、析构、RAII |
| 2 |
手写 shared_ptr |
⭐⭐⭐⭐ |
引用计数、控制块、线程安全 |
| 3 |
线程安全单例 |
⭐⭐ |
局部静态变量、双重检查锁定 |
| 4 |
生产者-消费者 |
⭐⭐⭐ |
条件变量、mutex、队列 |
| 5 |
环形缓冲区 |
⭐⭐⭐ |
原子操作、取模索引 |
| 6 |
无锁队列(MPMC) |
⭐⭐⭐⭐⭐ |
CAS、ABA 问题、内存序 |
| 7 |
内存池 |
⭐⭐⭐⭐ |
固定大小分配、链表管理 |
| 8 |
LRU 缓存 |
⭐⭐⭐ |
list + unordered_map、线程安全 |
| 9 |
快速排序(模板版) |
⭐⭐ |
模板、递归、partition |
| 10 |
线程池 |
⭐⭐⭐⭐ |
future/promise、任务队列、条件变量 |
十、大厂面试差异
阿里 vs 腾讯 vs 字节
| 维度 |
阿里巴巴 |
腾讯 |
字节跳动 |
| 算法难度 |
⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| C++ 基础深度 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐ |
| 操作系统 |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
| 网络编程 |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
| STL 源码深度 |
⭐⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐ |
| 现代 C++ 新特性 |
⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 并发编程 |
⭐⭐⭐ |
⭐⭐⭐⭐ |
⭐⭐⭐⭐⭐ |
| 设计模式 |
⭐⭐⭐⭐ |
⭐⭐⭐ |
⭐⭐⭐ |
十一、分岗位高频题
后端/高性能服务器方向
- epoll 原理、ET/LT 模式、Reactor/Proactor 模式
- 零拷贝(sendfile/mmap/io_uring)
- C++20 协程调度器设计
- 手写线程池、内存池
- TCP 三次握手/四次挥手、TIME_WAIT
- 分布式锁(Redis/ZK)、雪花算法 ID 生成
嵌入式/物联网方向
volatile 深入理解、内存对齐
- 中断服务程序(ISR)注意事项
- 为什么嵌入式慎用异常和虚函数(代码体积、确定性)
- RTOS 任务调度、优先级反转
- 位操作技巧、寄存器编程
static_assert 编译期检查
游戏开发/图形引擎方向
- 缓存友好性设计、SIMD 优化
- 面向数据设计(DOD)vs 面向对象设计(OOP)
- ECS 架构(组件-实体-系统)
- 渲染管线、矩阵/四元数运算
- 内存分配策略(栈分配器、池分配器)
高频交易/金融系统方向
- 纳秒级低延迟优化
- 缓存行对齐(
alignas(64))
- 分支预测优化(
likely/unlikely)
- NUMA 架构优化
- 无锁数据结构(RCU、Hazard Pointer)
memory_order 深入理解
附录:推荐准备路线
| 阶段 |
时间 |
内容 |
| 第1阶段 |
1-2周 |
精读《Effective C++》,手写 String/shared_ptr/LRU,完成基础题 100 道 |
| 第2阶段 |
2-3周 |
按岗位方向专精:后端(网络+并发)、嵌入式(RTOS+驱动)、游戏(图形学) |
| 第3阶段 |
1-2周 |
准备 2 个深度项目,能清晰讲解技术选型和性能优化证据;重点复习 C++20/23 新特性 |
2025-2026 年趋势总结 :C++ 面试呈现 基础深度 + 现代特性 + 岗位专精 三管齐下的趋势。C++20 的 Concepts、协程、Ranges 以及 C++23 的 std::expected 等新特性在字节等大厂面试中权重越来越高,无锁编程、内存模型、协程调度等高并发主题是高级岗位的必备考点。
十二、Lambda 表达式深入
Lambda 表达式在函数中的使用场景
// 1. 作为回调/谓词直接传入 STL 算法
std::vector<int> v = {1, 2, 3, 4, 5};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
// 2. 捕获局部变量作为回调(替代 std::bind)
int threshold = 10;
auto it = std::find_if(v.begin(), v.end(), [threshold](int x) {
return x > threshold; // 捕获外部变量 threshold
});
// 3. 作为异步任务的回调
auto future = std::async(std::launch::async, [&data]() {
return processData(data); // 引用捕获,直接操作外部数据
});
// 4. 在类成员函数中使用,捕获 this
class Widget {
int threshold_ = 5;
public:
void filter(std::vector<int>& input) {
input.erase(
std::remove_if(input.begin(), input.end(),
[this](int x) { return x < threshold_; } // 捕获 this 访问成员
),
input.end()
);
}
};
Lambda 捕获方式
| 捕获方式 |
说明 |
对捕获变量的影响 |
[] |
不捕获 |
只能访问全局/静态变量 |
[=] |
按值捕获所有 |
Lambda 内部有一份拷贝,不影响外部 |
[&] |
按引用捕获所有 |
修改会影响外部,注意悬挂引用 |
[x, &y] |
x 按值,y 按引用 |
混合捕获 |
[this] |
捕获 this 指针 |
可访问类成员 |
[*this] (C++17) |
拷贝整个对象 |
Lambda 持有对象的拷贝 |
[x = std::move(obj)] |
初始化捕获 (C++14) |
移动语义捕获 |
Lambda 本质
编译器将 lambda 展开为一个匿名函数对象(仿函数),捕获列表对应成员变量,operator() 对应函数体。
十三、操作系统基础
虚拟内存 vs 物理内存
| 特性 |
虚拟内存 |
物理内存 |
| 定义 |
每个进程看到的连续地址空间(逻辑概念) |
实际安装在硬件上的 RAM 条 |
| 大小 |
32位系统 4GB,64位可达 256TB |
有限,如 8GB/16GB |
| 连续性 |
逻辑上连续 |
通过页表映射,物理上可以不连续 |
| 隔离性 |
进程间天然隔离(A 进程地址 0x1000 ≠ B 进程地址 0x1000) |
共享同一物理空间 |
| 管理单元 |
页面 (Page),通常 4KB |
页面帧 (Page Frame) |
核心机制------页表映射:
虚拟地址 ──→ [页表] ──→ 物理地址
0x7fff1234 MMU查询 0xa3b41234
页表项包含:
- 物理页帧号
- 有效位(是否在物理内存中)
- 脏位(是否被修改过)
- 访问位(LRU 换出用)
缺页中断(Page Fault):访问的虚拟页不在物理内存时触发,OS 从磁盘(swap)换入到物理内存。
TLB(快表):页表缓存,加速虚拟地址翻译。TLB miss 开销很大(需多次内存访问查页表)。
内存管理理解
堆上内存通过 new/malloc 分配,程序员手动管理生命周期。现代 C++ 通过 RAII + 智能指针将动态内存自动回收。栈上内存编译器自动管理,作用域结束自动释放。
常见问题:
- 内存泄漏 :
new 了没 delete → 用智能指针解决
- 悬垂指针 :指向已释放内存的指针 → 释放后置
nullptr
- 重复释放 :对同一地址
delete 两次 → 智能指针的引用计数解决
- 缓冲区溢出 :越界写入 → 用
std::vector、std::string 替代裸数组
- 内存碎片:频繁分配释放小对象 → 内存池
可重入函数(Reentrant Function)
| 特性 |
可重入函数 |
不可重入函数 |
| 定义 |
被中断后再次调用仍正确 |
重入会导致数据错乱 |
| 静态/全局变量 |
不使用 |
使用 |
| 动态内存分配 |
不使用非原子的 malloc |
可能使用 |
| 标准库函数 |
strcpy、memcpy(只读输入) |
strtok(静态缓冲区) |
| 典型场景 |
中断服务程序 (ISR)、信号处理函数 |
普通应用代码 |
// ❌ 不可重入:使用了静态变量保存状态
char* strtok(char* str, const char* delim) {
static char* saved; // 静态变量保存进度,重入时会被覆盖
// ...
}
// ✅ 可重入版本:状态由调用者传入
char* strtok_r(char* str, const char* delim, char** saveptr) {
// 状态保存在调用者提供的指针中
}
十四、IO 多路复用
select / poll / epoll 对比
| 特性 |
select |
poll |
epoll |
| fd 数量限制 |
默认 1024(FD_SETSIZE) |
无限制 |
无限制 |
| 数据结构 |
fd_set(位图) |
pollfd 数组 |
红黑树 + 就绪链表 |
| 扫描方式 |
每次遍历所有 fd → O(n) |
每次遍历所有 fd → O(n) |
只遍历就绪 fd → O(1) |
| 内核态/用户态拷贝 |
每次调用拷贝整个 fd 集合 |
每次调用拷贝整个 pollfd 数组 |
fd 只注册一次,epoll_wait 只返回就绪事件 |
| 触发模式 |
水平触发 (LT) |
水平触发 (LT) |
LT + ET(边缘触发) |
| 适用场景 |
fd 少、精确时间 |
中等规模 |
大规模并发连接(C10K) |
水平触发 (LT) vs 边缘触发 (ET)
| 特性 |
LT (Level Triggered) |
ET (Edge Triggered) |
| 通知方式 |
数据未读完下次继续通知 |
只在状态变化时通知一次 |
| 编程复杂度 |
低 |
高(需循环读直到 EAGAIN) |
| 惊群问题 |
更容易出现 |
相对少 |
| 适用场景 |
简单可靠 |
高性能场景 |
// epoll 基本流程
int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event events[MAX_EVENTS];
while (running) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
// 处理新连接
int client = accept(listen_fd, ...);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client;
epoll_ctl(epfd, EPOLL_CTL_ADD, client, &ev);
} else {
// 处理客户端数据
char buf[4096];
while (true) { // ET 模式必须循环读完
int n = read(events[i].data.fd, buf, sizeof(buf));
if (n <= 0) break;
}
}
}
}
十五、平台与架构
Windows / Linux / ARM / x86 的关系
┌─────────────────────────────────────────────┐
│ 应用层(C++ 代码) │
├─────────────────────────────────────────────┤
│ 操作系统 (OS) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Windows │ │ Linux │ │
│ │ (NT 内核) │ │ (宏内核) │ │
│ └──────┬───────┘ └──────┬───────┘ │
├──────────┼─────────────────┼─────────────────┤
│ │ 指令集架构 │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ │
│ │ x86-64 │ │ ARM (aarch64)│ │
│ │ (Intel/AMD) │ │ (Apple M/高通) │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────┘
| 维度 |
Windows |
Linux |
| 内核类型 |
NT 混合内核 |
宏内核 (Monolithic) |
| 线程模型 |
系统线程为主 |
pthread / std::thread |
| 异步 IO |
IOCP (IO Completion Port) |
epoll / io_uring |
| 动态库 |
.dll |
.so |
| 路径分隔符 |
\ |
/ |
| 换行符 |
\r\n |
\n |
| ABI |
MSVC ABI(不兼容跨编译器) |
Itanium ABI(GCC/Clang 兼容) |
| 维度 |
x86-64 |
ARM (aarch64) |
| 设计哲学 |
CISC(复杂指令集) |
RISC(精简指令集) |
| 指令编码 |
变长(1-15 字节) |
定长 4 字节 |
| 内存模型 |
强内存模型(TSO) |
弱内存模型 |
| 典型设备 |
桌面/服务器 PC |
手机、嵌入式、Mac (M1-4) |
| 功耗 |
较高 |
较低 |
对 C++ 开发的影响:
- 不同平台/架构的
sizeof(long) 可能不同(x86 Linux 8 字节,Win64 4 字节)
- 原子操作在 ARM 上有真实内存屏障开销,x86 上 acquire/release 几乎零开销
std::thread::native_handle() 返回不同类型(Windows: HANDLE,Linux: pthread_t)
十六、调试与版本控制
GDB 常用命令
| 命令 |
说明 |
gdb ./program |
启动调试 |
gdb ./program core |
分析 core dump |
run / r |
运行程序 |
break file.cpp:42 / b main |
设置断点 |
continue / c |
继续执行 |
next / n |
单步执行(不进入函数) |
step / s |
单步执行(进入函数) |
finish |
执行到当前函数返回 |
print x / p x |
打印变量值 |
backtrace / bt |
查看调用栈 |
frame N |
切换到第 N 帧 |
info locals |
查看当前帧所有局部变量 |
info args |
查看当前帧函数参数 |
info threads |
查看所有线程 |
thread N |
切换到第 N 号线程 |
watch x |
监视变量 x 的变化 |
list |
显示源码 |
disassemble |
反汇编 |
quit |
退出 |
在 GDB 中查看崩溃点变量状态
# 1. 打开 core dump 文件
gdb ./program core
# 2. 查看崩溃时的调用栈
bt
bt full # 显示所有帧的局部变量
# 3. 切换到崩溃帧
frame 0 # 0 号帧通常是崩溃点
# 4. 查看所有局部变量
info locals
# 5. 查看函数参数
info args
# 6. 打印具体变量
print var_name
print *ptr # 解引用指针
print vec.size() # 可调用 STL 容器方法
print/x var # 十六进制格式打印
# 7. 查看寄存器状态
info registers
# 8. 查看内存内容
x/16x $rsp # 查看栈顶 16 个四字节
x/s str_ptr # 以字符串形式查看内存
# 9. 多线程崩溃分析
info threads # 列出所有线程
thread apply all bt # 查看所有线程的调用栈
前提 :需要有调试符号(编译时加 -g)且 core dump 大小限制未被关闭(ulimit -c unlimited)。
Git 常用操作
回退到之前的 commit
# 方式1:git reset --- 修改 HEAD 位置
git reset --soft HEAD~1 # 回退1个提交,改动保留在暂存区
git reset --mixed HEAD~1 # 回退1个提交,改动保留在工作区(默认)
git reset --hard HEAD~1 # 回退1个提交,改动完全丢弃(危险!)
git reset --hard <commit-hash> # 回退到指定 commit
# 方式2:git revert --- 生成新 commit 撤销旧 commit(安全,适合已有 push 的分支)
git revert HEAD # 创建一个新 commit 来撤销最近一次提交
git revert <commit-hash> # 撤销指定 commit
| 操作 |
是否改写历史 |
适用场景 |
git reset |
是(本地) |
本地 commit 未 push |
git revert |
否(新增 commit) |
已经 push 到远程 |
创建分支与合并
# 创建分支
git branch feature-xxx # 创建分支(不切换)
git checkout -b feature-xxx # 创建并切换到新分支
git switch -c feature-xxx # 同上,Git 2.23+ 推荐
# 推送分支到远程
git push -u origin feature-xxx
# 合并分支
git checkout main # 先切到目标分支
git merge feature-xxx # 将 feature-xxx 合并到当前分支
# 解决冲突后
git add <冲突文件>
git commit # 完成合并
# 变基合并(保持线性历史)
git checkout feature-xxx
git rebase main # 将 feature-xxx 的提交放到 main 最新提交之后
git checkout main
git merge feature-xxx # 此时是 fast-forward,无额外 merge commit
Git 分支管理策略
main / master ← 生产就绪代码,只通过 PR 合并
└── develop ← 开发主线
├── feat/xxx ← 功能分支(短生命周期,合入后删除)
├── fix/xxx ← Bug 修复分支
└── release/x ← 发布分支
常见分支数量:日常同时活跃 2-5 个分支(1 个开发主线 + 若干功能/修复分支)。遵循"分支宜短命"原则------功能完成后尽快合入主线并删除旧分支。