现代C++
"现代 C++"通常指 C++11 以及之后的标准引入的一系列写法和库工具。它不是把旧 C++ 全部推翻,而是在原有语法之上,提供更安全、更清晰、更适合工程开发的表达方式。
本章按"由浅入深、从常用到进阶"的顺序组织:先学每天都会用到的语法,再学生命周期和资源管理,接着学回调、数据表达、时间、文件和并发,最后了解模块化编译。
本章例程约定
为了方便你直接复制验证,本章各小节的"示例代码"遵循这些约定:
- 示例代码都包含完整
#include。 - 示例代码都包含
main函数。 - 示例代码可以复制到单个
.cpp文件中编译运行。 - 每个示例代码下方都给出运行结果。
- 示例尽量一次只增加一个新概念,避免一个例子塞太多东西。
文中的"常见错误"用于说明不要这样写。为了避免误复制,错误写法会尽量放在文字说明或短片段中,不当作可运行示例。
学习顺序
| 阶段 | 先解决的问题 | 对应章节 |
|---|---|---|
| 基础表达 | 少写重复类型,减少空指针和枚举混用,遍历更清楚 | auto、nullptr、using、enum class、范围 for、结构化绑定 |
| 生命周期 | 资源什么时候释放,谁拥有对象,大对象怎么高效传递 | constexpr、RAII、智能指针、右值引用和移动语义 |
| 回调和数据模型 | 小函数、回调保存、参数适配、可能没有值、多种类型之一 | Lambda、std::function、std::bind、std::optional、std::variant、std::span |
| 系统能力 | 格式化输出、时间、并发、文件系统、模块化 | std::format / std::print、std::chrono、并发编程、std::filesystem、modules |
现代 C++ 的重点不是"语法更新",而是"把意图写清楚"。例如:
nullptr让空指针和整数0分开。enum class让不同枚举类型不能随便混用。- RAII 和智能指针把资源释放交给对象生命周期。
- Lambda 让局部回调写在使用现场。
std::function把不同类型的可调用对象统一保存和传递。std::format/std::print让格式化输出更简洁,并保持类型安全。
旧写法和现代写法的关系
很多现代 C++ 特性都来自一个朴素问题:旧写法能用,但在复杂场景里容易出错。
| 旧写法 | 主要问题 | 现代写法 |
|---|---|---|
NULL / 0 表示空指针 | 会和整数重载混淆 | nullptr |
typedef 写类型别名 | 模板别名不直观 | using |
| 普通枚举 | 枚举名污染作用域,可隐式转整数 | enum class |
手动 new/delete | 提前返回、异常、重复释放都容易出错 | RAII、std::unique_ptr、std::shared_ptr |
手动 lock/unlock | 忘记解锁会死锁,异常路径更危险 | std::lock_guard、std::unique_lock |
| 远处定义普通函数做简单回调 | 局部逻辑被拆散,不能方便携带上下文 | Lambda |
| 只用函数指针保存回调 | 不能保存有捕获的 lambda 和函数对象 | std::function |
| 手写适配函数调整参数 | 代码重复,旧接口适配麻烦 | std::bind 或 Lambda |
| 用特殊值表示失败 | -1、空字符串等魔法值语义不清 | std::optional |
用 union 表示多种类型 | 访问错误类型会产生未定义行为 | std::variant |
| 手写时间单位换算 | 秒、毫秒、微秒容易混 | std::chrono |
| 平台相关文件 API | Windows/Linux 写法不同 | std::filesystem |
学习每个特性时,可以按三个问题来理解:
- 旧写法哪里容易错?
- 现代写法如何把意图写进类型或语法里?
- 在什么场景下差异才明显?
比如一个智能指针示例如果只在 main 里创建对象然后正常结束,看起来和手动 delete 差不多;一旦出现提前 return、异常、跨函数传递,RAII 的价值就会立刻显现。一个 Lambda 如果只立刻调用,按值捕获和按引用捕获都可能没事;一旦保存到回调、线程、定时器里,生命周期差异就会变得非常重要。
推荐学习路线
让代码更清楚
先学习 auto、nullptr、using、enum class、范围 for 和结构化绑定。这些内容难度不高,但能明显减少冗长代码和低级错误。
这一阶段重点记住:
auto不是弱类型,只是让编译器帮你写类型。nullptr只表示空指针,不表示整数。enum class默认不和整数混用。- 范围 for 默认用
const auto&遍历大对象。 - 结构化绑定适合解包
pair、tuple、结构体和map元素。
理解生命周期
接着学习 constexpr、RAII、智能指针和移动语义。这一阶段比语法更重要的是思维方式:对象什么时候创建,什么时候销毁,谁拥有它,谁只是借用它。
建议按这个顺序理解:
- RAII:资源和对象生命周期绑定。
unique_ptr:独占所有权,默认选择。shared_ptr:确实需要多个所有者时才用。weak_ptr:只观察,不延长生命周期,用来打破循环引用。- 移动语义:把资源转移出去,避免大对象深拷贝。
掌握回调和数据表达
Lambda、std::function、std::bind 三章要连起来看,但不要混在一起背:
| 工具 | 解决的问题 | 重点 |
|---|---|---|
| Lambda | 在使用现场写一个小函数 | 捕获列表、参数、返回值、生命周期 |
std::function | 统一保存和传递不同类型的可调用对象 | 类型擦除、回调成员变量、回调容器 |
std::bind | 适配已有函数的参数 | 固定参数、调整顺序、绑定成员函数 |
std::optional、std::variant、std::span 则分别解决"可能没有值"、"几种类型之一"、"借用一段连续数据"的问题。
走向工程能力
最后学习格式化输出、时间、并发、文件系统和 modules。这些内容更接近真实项目:
std::format/std::print比stringstream简洁,比printf类型安全。std::chrono避免手写时间单位换算。- 并发编程要先保证正确,再考虑速度。
std::filesystem让路径、目录、文件信息处理跨平台。- modules 是 C++20 引入的新编译模型,目前应先理解概念,再根据工具链支持决定是否使用。
实战练习建议
- 学
nullptr:写两个重载函数,分别接收int和int*,观察0、NULL、nullptr的区别。 - 学
enum class:把普通枚举换成枚举类,看看哪些隐式转换被禁止。 - 学智能指针:先写一个提前
return的手动new/delete例子,再改成unique_ptr。 - 学 Lambda:分别用按值捕获和按引用捕获,观察外部变量是否变化。
- 学
std::function:把多个不同 lambda 放进同一个vector,统一执行。 - 学并发:让两个 1 秒等待任务先顺序执行,再放进两个线程中执行,对比耗时。