文章目录

C++ 进阶学习笔记

适合目标:在完成基础入门后,补齐现代 C++ 核心能力,能应对中高级面试题,也能理解真实工程代码。
学习重点:RAII、智能指针、右值引用、移动语义、模板、STL 深入、并发、设计原则。
学习原则:每学一个进阶特性,都要回答“它是为了解决什么问题”。


目录

  1. 进阶学习主线
  2. RAII
  3. 智能指针
  4. 左值、右值与移动语义
  5. 完美转发
  6. 模板与泛型编程
  7. STL 深入
  8. lambda 与函数对象
  9. 并发基础
  10. 设计与工程实践
  11. 高频面试题
  12. 学习路径建议
  13. 一页速记总结

1. 进阶学习主线

C++ 进阶不是继续记更多语法,而是理解现代 C++ 怎样解决下面这些问题:

  1. 资源怎么安全管理
  2. 对象怎么少拷贝、少分配
  3. 泛型代码怎么既复用又高性能
  4. 多线程代码怎么避免竞态和死锁
  5. 大型工程里怎么控制复杂度

所以进阶阶段的主线可以总结成:

资源管理 -> 性能优化 -> 泛型抽象 -> 并发控制 -> 工程设计


2. RAII

RAII 全称是 Resource Acquisition Is Initialization。

核心思想:

  1. 资源在对象构造时获取
  2. 资源在对象析构时释放
  3. 用对象生命周期绑定资源生命周期

这样做的最大价值是:

  1. 即使函数提前 return,也能自动释放资源
  2. 即使发生异常,也更容易保证清理逻辑执行

示例:

class FileGuard {
public:
  FileGuard(const string& path) {
    cout << "open file: " << path << endl;
  }

  ~FileGuard() {
    cout << "close file" << endl;
  }
};

void work() {
  FileGuard file("a.txt");
}

面试里常见表达:

RAII 是现代 C++ 资源管理的核心思想,智能指针、lock_guard、本地对象清理都建立在它上面。


3. 智能指针

现代 C++ 最重要的一块之一,就是尽量少手写裸 new/delete

3.1 unique_ptr

独占所有权。

std::unique_ptr<int> p = std::make_unique<int>(10);

特点:

  1. 同一时刻只有一个拥有者
  2. 不可拷贝,可移动
  3. 开销小,最常优先使用

3.2 shared_ptr

共享所有权,内部维护引用计数。

std::shared_ptr<int> p1 = std::make_shared<int>(10);
std::shared_ptr<int> p2 = p1;

特点:

  1. 多个对象可共同持有同一资源
  2. 最后一个持有者析构时释放资源
  3. 有额外计数成本

3.3 weak_ptr

解决 shared_ptr 循环引用问题。

std::weak_ptr<int> wp;

使用场景:

  1. 观察资源但不拥有资源
  2. 打破双向引用

3.4 智能指针选型

优先级通常是:

  1. 能用栈对象就别上堆
  2. 必须动态分配时优先 unique_ptr
  3. 确实需要共享所有权再用 shared_ptr
  4. 观察关系用 weak_ptr

4. 左值、右值与移动语义

这一部分是很多进阶面试题的核心。

4.1 左值和右值

先用最实用的方式理解:

  1. 左值通常有名字、能取地址、生命周期较稳定
  2. 右值通常是临时对象,马上就会销毁
int a = 10;
int b = a + 5;

这里:

  1. a 是左值
  2. a + 5 产生的临时结果更接近右值

4.2 为什么要有移动语义

因为很多对象内部持有堆内存,如果每次传递都深拷贝,成本很高。

移动语义的思想是:

既然源对象马上不用了,那就把资源“搬过来”,不要重新拷贝一遍。

4.3 右值引用

int&& x = 10;

右值引用主要是为移动语义和完美转发服务的,不是单纯为了多一个引用语法。

4.4 移动构造和移动赋值

class Buffer {
public:
  Buffer(Buffer&& other) noexcept {
  }

  Buffer& operator=(Buffer&& other) noexcept {
    return *this;
  }
};

理解重点:

  1. 移动是“转移资源所有权”
  2. 被移动对象要保持“可析构、可赋值”的有效状态
  3. noexcept 往往影响容器是否愿意使用移动操作

4.5 std::move

std::move 本身不移动,它只是把对象转成右值语义,告诉编译器“这个对象可以被搬走”。


5. 完美转发

完美转发解决的问题是:

模板函数接收到参数后,怎样保持它原本的左值/右值属性继续传下去。

template <typename T>
void wrapper(T&& value) {
  func(std::forward<T>(value));
}

要点:

  1. T&& 在模板推导场景下可能是万能引用
  2. std::forward<T> 用于保留值类别
  3. 常用于工厂函数、通用封装和容器实现

6. 模板与泛型编程

6.1 函数模板

template <typename T>
T add(T a, T b) {
  return a + b;
}

6.2 类模板

template <typename T>
class Box {
public:
  T value;
};

模板的价值:

  1. 代码复用
  2. 编译期生成具体类型版本
  3. 通常兼顾抽象能力和性能

6.3 模板实例化

模板不是写完就真的生成机器码,通常在使用到具体类型时才实例化。

6.4 变参模板

template <typename... Args>
void log(Args... args) {
}

应用场景:

  1. 通用打印
  2. emplace_back
  3. 工厂封装

7. STL 深入

7.1 vector 扩容机制

vector 底层是一段连续内存。

关键点:

  1. 扩容时可能整体搬迁
  2. 原来的迭代器、引用、指针可能失效
  3. 尾插通常均摊 O(1)

7.2 map 和 unordered_map 选型

  1. 需要有序遍历用 map
  2. 更关注平均查找效率常用 unordered_map
  3. 哈希冲突严重时 unordered_map 表现会波动

7.3 迭代器失效

这是非常高频的面试和 bug 来源。

例如:

  1. vector 扩容后迭代器可能失效
  2. erase 后当前位置及其后的迭代器可能失效
  3. 不同容器的失效规则不同

7.4 常用算法

sort(nums.begin(), nums.end());
reverse(nums.begin(), nums.end());
auto it = find(nums.begin(), nums.end(), 3);

进阶学习不能只会容器,还要会配合 <algorithm> 使用。


8. lambda 与函数对象

8.1 lambda

auto add = [](int a, int b) {
  return a + b;
};

优势:

  1. 适合短逻辑内联定义
  2. 和 STL 算法天然配合
  3. 能捕获上下文变量

8.2 捕获列表

int x = 10;
auto f1 = [x]() { return x; };
auto f2 = [&x]() { x++; };

区别:

  1. [x] 值捕获
  2. [&x] 引用捕获

8.3 仿函数

重载 operator() 的对象。

class Add {
public:
  int operator()(int a, int b) const {
    return a + b;
  }
};

9. 并发基础

9.1 线程

#include <thread>

void task() {}

std::thread t(task);
t.join();

9.2 互斥锁

#include <mutex>

std::mutex mtx;

共享数据修改时要注意竞态条件。

9.3 lock_guard

{
  std::lock_guard<std::mutex> lock(mtx);
}

这又是 RAII 的典型应用。

9.4 死锁

常见成因:

  1. 多把锁获取顺序不一致
  2. 忘记释放锁
  3. 锁粒度设计不合理

9.5 原子操作

#include <atomic>

std::atomic<int> cnt = 0;

适用于简单共享状态,但它不能替代所有同步问题。


10. 设计与工程实践

10.1 Rule of Three / Five

如果类需要自定义以下资源管理函数中的一些,通常要整体考虑:

  1. 析构函数
  2. 拷贝构造
  3. 拷贝赋值
  4. 移动构造
  5. 移动赋值

现代 C++ 更常说 Rule of Five。

10.2 组合优于继承

工程里不要为了“像面向对象”就滥用继承。

很多时候组合更稳:

  1. 耦合更低
  2. 更容易替换
  3. 结构更清晰

10.3 接口设计建议

  1. 明确所有权归属
  2. 尽量用值语义或智能指针表达生命周期
  3. 尽量减少裸指针暴露范围
  4. 常量语义尽量通过 const 表达清楚

11. 高频面试题

11.1 shared_ptr 为什么可能有循环引用

因为两个对象互相持有 shared_ptr,引用计数都不会归零,导致资源无法释放。解决方式通常是其中一边改成 weak_ptr

11.2 什么是移动语义

本质是把原对象持有的资源转移给目标对象,避免深拷贝,提高性能,常与右值引用一起出现。

11.3 std::move 一定会触发移动吗

不一定。它只是一个类型转换,真正是否调用移动构造/移动赋值,还取决于目标类型是否支持移动以及上下文是否匹配。

11.4 vector 扩容有什么影响

可能重新申请更大的连续空间并搬迁元素,导致原有地址、引用、迭代器失效。

11.5 模板和宏的区别

  1. 宏是预处理文本替换
  2. 模板是编译期类型化机制
  3. 模板更安全、可检查、可推导

12. 学习路径建议

建议按这个顺序推进:

  1. 先吃透 RAII 和智能指针
  2. 再补左值右值、移动语义、完美转发
  3. 接着学模板、变参模板、STL 深入
  4. 然后补线程、锁、原子变量
  5. 最后通过项目代码和面试题反复巩固

如果你的目标是求职,进阶阶段一定要会把“概念”翻译成“工程风险”:

  1. 为什么这里不能裸指针共享
  2. 为什么这里要避免不必要拷贝
  3. 为什么这里需要锁或原子变量
  4. 为什么这里迭代器会失效

13. 一页速记总结

  1. RAII 是现代 C++ 资源管理核心
  2. 智能指针核心是表达所有权与自动释放
  3. 右值引用和移动语义核心是避免不必要深拷贝
  4. 完美转发核心是保留参数原始值类别
  5. 模板核心是编译期泛型复用
  6. STL 进阶重点是容器特性、迭代器失效、算法配合
  7. 并发重点是竞态、锁、死锁、原子性
  8. 工程实践重点是生命周期、所有权、接口边界