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 关键字的多种用法
cpp
复制代码
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: 深拷贝与浅拷贝
cpp
复制代码
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++ 混合编程。
cpp
复制代码
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 子类对象时,如果基类析构函数不是虚函数,则只会调用基类析构,子类资源泄漏。
cpp
复制代码
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 + ...)
cpp
复制代码
// 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(概念)
编译期约束模板参数,让模板错误信息更清晰。
cpp
复制代码
#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(范围库)
管道式操作,惰性求值,零中间容器。
cpp
复制代码
#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。无栈协程,挂起时状态存于堆分配的协程帧。
cpp
复制代码
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 拥有对象。禁止拷贝,只能移动。
创建方式
cpp
复制代码
#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[]
所有权转移 --- 只能移动
cpp
复制代码
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)); // 临时对象直接移动
常用操作
cpp
复制代码
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) { /* 为空 */ }
自定义删除器
cpp
复制代码
// 场景: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);
实现原理(极简版)
cpp
复制代码
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:销毁控制块
创建方式
cpp
复制代码
// 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
拷贝与引用计数
cpp
复制代码
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 → 对象被释放
常用操作
cpp
复制代码
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 起废弃)
自定义删除器
cpp
复制代码
// 自定义删除器(与 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 需同步 │
└──────────────────────────────────────────────┘
cpp
复制代码
// ✅ 安全:不同线程持有各自的 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?
cpp
复制代码
// ❌ 错误的做法
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
cpp
复制代码
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)); // ❌ 编译错误
核心方法
cpp
复制代码
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 不再观察任何对象
打破循环引用(详细示例)
cpp
复制代码
#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 ⇢ 正确释放
实用场景:观察者模式 + 自动清理
cpp
复制代码
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 自动跳过
实用场景:缓存
cpp
复制代码
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 不移动任何东西! 它只是一个无条件的类型转换,真正的移动发生在移动构造函数/移动赋值运算符中。
具名右值引用变量是左值! 任何有名字的变量都是左值,即使类型是右值引用。
cpp
复制代码
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 当前位置及之后失效
cpp
复制代码
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
手写队列(数组实现 + 链表实现)
cpp
复制代码
// 数组实现(环形队列)
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 同步(必考)
cpp
复制代码
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); // 一定成功!
}
无锁自旋锁
cpp
复制代码
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)
cpp
复制代码
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)用法详解
cpp
复制代码
#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)
为什么要用线程池? 线程创建/销毁有开销,频繁创建会降低性能。线程池预先创建一组工作线程,任务到来时分配给空闲线程执行,任务完成后线程不销毁而是等待新任务。
cpp
复制代码
#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 局部静态变量线程安全) :
cpp
复制代码
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++ 可注册工厂 :
cpp
复制代码
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) :
cpp
复制代码
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 类(必考🔥)
cpp
复制代码
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
cpp
复制代码
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)
cpp
复制代码
class Singleton {
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;
};
4. 生产者-消费者模型
cpp
复制代码
#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)
cpp
复制代码
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)
cpp
复制代码
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. 内存池(简易版本)
cpp
复制代码
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 表达式在函数中的使用场景
cpp
复制代码
// 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)、信号处理函数
普通应用代码
cpp
复制代码
// ❌ 不可重入:使用了静态变量保存状态
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)
惊群问题
更容易出现
相对少
适用场景
简单可靠
高性能场景
cpp
复制代码
// 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 中查看崩溃点变量状态
bash
复制代码
# 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
bash
复制代码
# 方式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 到远程
创建分支与合并
bash
复制代码
# 创建分支
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 个开发主线 + 若干功能/修复分支)。遵循"分支宜短命"原则------功能完成后尽快合入主线并删除旧分支。