> For the complete documentation index, see [llms.txt](https://gists.lanlance.cn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gists.lanlance.cn/cs/cpp.md).

# C++

## 1. 预处理阶段能做什么

* 预处理不属于 C++ 语言，过多的预处理语句会扰乱正常的代码，除非必要，应当少用慎用；
* “#include”可以包含任意文件，所以可以写一些小的代码片段，再引进程序里；
* 头文件应该加上“Include Guard”，防止重复包含；
* “#define”用于宏定义，非常灵活，但滥用文本替换可能会降低代码的可读性；
* “条件编译”其实就是预处理编程里的分支语句，可以改变源码的形态，针对系统生成最合适的代码。

## 2. 编译阶段能做什么

* “属性”相当于编译阶段的“标签”，用来标记变量、函数或者类，让编译器发出或者不发出警告，还能够手工指定代码的优化方式。
* 标准属性不多，常用的有 \[\[noreturn]]（C++11）、\[\[deprecated]]（C++14）、\[\[nodiscard]]、\[\[fallthrough]]、\[\[maybe\_unused]]（C++17），C++20 又增加了 \[\[likely]]、\[\[unlikely]]、\[\[no\_unique\_address]]，C++23 增加 \[\[assume]]，C++26 又增加了 \[\[indeterminate]]（C++26 已于 2026 年 3 月定稿，是当前最新的标准版本）；我们也可以使用非官方的属性（如 gnu:: 前缀），需要加上名字空间限定。
* static\_assert 是“静态断言”，在编译阶段计算常数和类型，如果断言失败就会导致编译错误。它也是迈向模板元编程的第一步。
* 和运行阶段的“动态断言”一样，static\_assert 可以在编译阶段定义各种前置条件，充分利用 C++ 静态类型语言的优势，让编译器执行各种检查，避免把隐患带到运行阶段。

## 3. 面向对象编程

* “面向对象编程”是一种设计思想，要点是“抽象”和“封装”，“继承”“多态”是衍生出的特性，不完全符合现实世界。
* 在 C++ 里应当少用继承和虚函数，降低对象的成本，绕过那些难懂易错的陷阱。
* 使用特殊标识符“final”可以禁止类被继承，简化类的层次关系。
* 类有六大基本函数，对于重要的构造 / 析构函数，可以使用“= default”来显式要求编译器使用默认实现。
* “委托构造”和“成员变量初始化”特性可以让创建对象的工作更加轻松。
* 使用 using 或 typedef 可以为类型起别名，既能够简化代码，还能够适应将来的变化。

## 4. 自动类型推导

* “自动类型推导”是给编译器下的指令，让编译器去计算表达式的类型，然后返回给程序员。
* auto 用于初始化时的类型推导，总是“值类型”，也可以加上修饰符产生新类型。它的规则比较好理解，用法也简单，应该积极使用。
* decltype 使用类似函数调用的形式计算表达式的类型，能够用在任意场合，因为它就是 一个编译阶段的类型。
* decltype 能够推导出表达式的精确类型，但写起来比较麻烦，在初始化时可以采用 decltype(auto) 的简化形式。
* 因为 auto 和 decltype 不是“硬编码”的类型，所以用好它们可以让代码更清晰，减少后期维护的成本。

## 5. const & volatile & mutable

1. const

* 它是一个类型修饰符，可以给任何对象附加上“只读”属性，保证安全；
* 它可以修饰引用和指针，“const &”可以引用任何类型，是函数入口参数的最佳类型；
* 它还可以修饰成员函数，表示函数是“只读”的，const 对象只能调用 const 成员函数。

2. volatile

* 它表示变量可能会被“不被察觉”地修改，禁止编译器优化，影响性能，应当少用；“禁止优化”只是通俗说法，标准保证的是每次访问都真的读写内存、访问不可被省略或合并，它不提供原子性和内存序，多线程共享数据要用 atomic 或互斥量，而不是 volatile。

3. mutable

* 它用来修饰成员变量，允许 const 成员函数修改，mutable 变量的变化不影响对象的常量性，但要小心不要误用损坏对象。

尽可能多用 const，让代码更安全

## 6. 智能指针到底智能在哪里

* 智能指针使用 RAII 技术包装裸指针，能够自动释放内存，无需程序员干预，所以被称为“智能指针”；主流归类是 RAII 惯用法，也有观点视其为代理模式的具体应用（代理裸指针的所有权与访问）。
* 如果指针是“独占”使用，就应该选择 unique\_ptr，它为裸指针添加了很多限制，更加安全。
* 如果指针是“共享”使用，就应该选择 shared\_ptr，它的功能非常完善，用法几乎与原始指针一样。
* 应当使用工厂函数 make\_unique()、make\_shared() 来创建智能指针，强制初始化，而且还能使用 auto 来简化声明。
* shared\_ptr 有少量的管理成本，也会引发一些难以排查的错误，所以不要过度使用。
* weak\_ptr 是 shared\_ptr 的“观察者”，不增加引用计数，专门用来打破 shared\_ptr 的循环引用（两个对象互相持有 shared\_ptr 时引用计数永远不归零，内存无法释放）；需要访问对象时用 lock() 临时提升为 shared\_ptr。

## 7. C++ 中的异常处理

* 异常是针对错误码的缺陷而设计的，它不能被忽略，而且可以“穿透”调用栈，逐层传播到其他地方去处理；
* 使用 try-catch 机制处理异常，能够分离正常流程与错误处理流程，让代码更清晰；
* throw 可以抛出任何类型作为异常，但最好使用标准库里定义的 exception 类；
* 完全用或不用异常处理错误都不可取，而是应该合理分析，适度使用，降低异常的成本；
* 关键字 noexcept 标记函数不抛出异常，可以让编译器做更好的优化。

## 8. lambda 表达式

* lambda 表达式是一个闭包，能够像函数一样被调用，像变量一样被传递；
* 可以使用 auto 自动推导类型存储 lambda 表达式，但 C++ 鼓励尽量就地匿名使用，缩小作用域；
* lambda 表达式使用“\[=]”的方式按值捕获，使用“\[&]”的方式按引用捕获，空的“\[]”则是无捕获（也就相当于普通函数）；
* 捕获引用时必须要注意外部变量的生命周期，防止变量失效；
* C++14 里可以使用泛型的 lambda 表达式，相当于简化的模板函数；C++20 又允许显式写出模板参数列表（模板 lambda）。

## 9. C++ 中的字符串

* C++ 支持多种字符类型，常用的 string 其实是模板类 basic\_string 的特化形式；
* C++ 对 Unicode 的支持在逐步完善（C++11 引入 char16\_t/char32\_t 与 UTF-8 字符串字面量，C++20 引入 UTF-8 专用类型 char8\_t 与 std::format 格式化库，C++23 增加 std::print/std::println），但标准库仍缺少大小写转换、规范化等完整的 Unicode 处理设施，实际项目中建议尽量避开国际化和编码转化，必要时借助第三方库；
* 应当把 string 视为一个完整的字符串来操作，不要把它当成容器来使用；
* 字面量后缀“s”表示字符串类，可以用来自动推导出 string 类型；
* 原始字符串不会转义，是字符串的原始形态，适合在代码里写复杂的文本；

## 10. 详解 C++ 容器

* 标准容器可以分为三大类，即顺序容器、有序容器和无序容器；
* 所有容器中最优先选择的应该是 array 和 vector，它们的速度最快，开销最低；
* list 是链表结构，插入删除的效率高，但查找效率低；
* 有序容器（map/set）对 key 自动排序，查找效率高，但有插入成本；标准只要求对数复杂度、并未强制底层数据结构，主流实现（libstdc++、libc++、MSVC）均为红黑树；
* 无序容器是散列表结构，由 hash 值计算存储位置，查找和插入的成本都很低；
* 有序容器和无序容器都属于关联容器，元素有 key 的概念，操作元素实际上是在操作 key，所以要定义对 key 的比较函数或者散列函数。
* 迭代器失效要小心：vector 容量不足时会重新分配内存（倍数由实现决定，常见 1.5～2 倍），原有的迭代器、指针、引用全部失效；有序容器插入不影响已有迭代器、只有被删元素的迭代器失效，无序容器扩容（rehash）则所有迭代器都失效。

## 11. 多线程编程

* 多线程是并发最常用的实现方式，好处是任务并行、避免阻塞，坏处是开发难度高，有数据竞争、死锁等很多“坑”；
* call\_once() 实现了仅调用一次的功能，避免多线程初始化时的冲突；
* thread\_local 实现了线程局部存储，让每个线程都独立访问数据，互不干扰；
* atomic 实现了原子化变量，可以用作线程安全的计数器，也可以实现无锁数据结构；
* async() 启动一个异步任务并返回 future，用 get() 拿结果、也能接住异常，比手工管理线程省事；但它并不保证新开线程——默认策略是 launch::async | launch::deferred，可能推迟到 get()/wait() 时在当前线程同步执行，要确保并发必须显式指定 std::launch::async；另外 future 析构时若任务还没结束会阻塞等待；
* mutex 配合 lock\_guard / unique\_lock 用 RAII 自动加解锁，避免忘记 unlock；多把锁要么固定加锁顺序，要么用 std::scoped\_lock（C++17）/ std::lock() 一次锁住，防止死锁。

## 12. 右值引用与移动语义

* 右值引用（T&&，C++11）用于绑定临时对象和即将销毁的对象，让“搬运”资源（堆内存、文件句柄）成为可能，避免一次昂贵的拷贝；
* std::move() 并不移动任何东西，它只是把左值强制转换成右值引用，真正的移动发生在移动构造 / 移动赋值函数里；
* 被移动过的对象处于“有效但未指定”的状态，只能析构或重新赋值，不要假设它还拿着原来的资源；
* 模板里的 std::forward() 做的是完美转发，按参数原来的值类别转发出去，接收端要写成 T&&（万能引用 / 转发引用）才能配合；
* 自己写了拷贝构造、拷贝赋值或者析构函数，编译器就不会再隐式生成移动构造 / 移动赋值（Rule of five）；
* 移动构造 / 移动赋值声明为 noexcept 很有价值：容器扩容时标准库用类似 move\_if\_noexcept 的策略选择，不是 noexcept 的移动构造反而会退回拷贝；
* 返回值优化（RVO / NRVO）能直接省掉返回局部对象时的一次拷贝或移动，所以“返回对象”通常比“输出参数”更好。

## 13. 虚函数的实现原理

* 多态（动态绑定）靠虚函数实现：带虚函数的类有一张虚函数表（vtable），对象里用隐藏的指针（vptr）指向本类的 vtable，调用虚函数时顺着 vptr 找到实际要执行的函数地址；
* vtable 每个类只有一份、编译期生成，vptr 在构造 / 析构过程中被设置成本类的 vtable，所以在构造函数和析构函数里调用虚函数不会有“多态”效果；
* 基类的析构函数应该声明为 virtual，否则通过基类指针 delete 派生类对象只会执行基类析构函数，派生类的资源泄漏（这也是少用继承的理由之一）；
* 用 override 显式声明重写、用 final 禁止继续重写（final 也可以直接禁止类被继承），能让编译器帮你发现签名不匹配；
* 纯虚函数（= 0）让类变成抽象类，不能实例化，常被当作接口使用；
* 虚函数有成本（一次间接寻址、往往无法内联），只在真正需要多态时才使用。
