A Tour of C++(第二版)
“When you wish to instruct, be brief.” — Cicero(扉页题词)
C++ 之父 Bjarne Stroustrup 的现代 C++ 全景导览。基于 C++17 标准,同时前瞻 C++20 的 Concepts 和 Modules。本书不是参考手册,而是一次”城市观光”——用几百页篇幅带你快速领略现代 C++ 的核心风貌,帮助有经验的程序员快速建立对现代 C++ 的整体认知。
核心定位
- 目标读者:有其他语言编程经验的程序员,或想从旧版 C++ 升级到现代 C++ 的开发者
- 阅读时长:几小时可通读,掌握全景;但精通需要多年实践
- 写作风格:以编程范式(过程式、面向对象、泛型)为主线组织语言特性,而非逐条罗列语法
- 配套资源:C++ Core Guidelines(本书是核心准则的重要来源之一)
- 与《The C++ Programming Language》的关系:本书是 TC++PL 的精简导览版,想看细节再去查 TC++PL
全书结构(16 章,两大板块)
第一部分:语言特性(第 1-7 章)
| 章节 | 主题 | 核心内容 |
|---|---|---|
| 1 | The Basics | C++ 基础模型:编译/链接、静态类型、内置类型、指针/数组/引用、常量、作用域 |
| 2 | User-Defined Types | 抽象机制入门:struct、class、union、enum,用户自定义类型优于内置类型 |
| 3 | Modularity | 模块化:分离编译、头文件、命名空间、异常处理、Modules (C++20) |
| 4 | Classes | 类的三种形态:具体类型、抽象类型、类层次结构;虚函数与多态 |
| 5 | Essential Operations | 五大特殊成员:构造/析构/拷贝构造/移动构造/赋值;RAII 资源管理 |
| 6 | Templates | 模板:参数化类型与操作、函数对象、lambda、模板机制 |
| 7 | Concepts and Generic Programming | 概念 (C++20)、泛型编程思想、可变参数模板、模板编译模型 |
第二部分:标准库(第 8-15 章)
| 章节 | 主题 | 核心内容 |
|---|---|---|
| 8 | Library Overview | 标准库总览:组件分类、头文件、命名空间 |
| 9 | Strings and Regular Expressions | string、string_view、正则表达式 |
| 10 | Input and Output | iostream 流 I/O、格式化、文件流、字符串流、文件系统 |
| 11 | Containers | vector、list、map、unordered_map 等 STL 容器 |
| 12 | Algorithms | STL 算法、迭代器、谓词、并行算法 (C++17)、Ranges 概念 |
| 13 | Utilities | 智能指针、span、array、bitset、tuple、时间、类型函数 |
| 14 | Numerics | 数学函数、数值算法、复数、随机数、valarray、数值极限 |
| 15 | Concurrency | 线程、互斥量、future/promise、条件变量、并行算法 |
第 16 章:History and Compatibility
C++ 历史、特性演进时间线、C/C++ 兼容性、参考文献。
核心思想与关键观点
1. C++ 的设计哲学
- 多范式语言:同时支持过程式、面向对象、泛型编程,不强迫任何一种风格
- 零成本抽象(Zero-overhead Abstraction):你不用的特性不会为你付出代价;你用的特性,手写也不会更好
- 静态类型:每个实体的类型在编译时确定,类型决定可执行的操作
- 与硬件直接映射:内置类型和操作直接反映传统计算机硬件的能力
- 标准库是一等公民:标准库本身就是用 C++ 写的,证明了语言的表达力和效率
2. 用户自定义类型优先
“User-defined types are often preferred over built-in types because they are easier to use, less error-prone, and typically as efficient.”
C++ 的抽象机制让程序员能构建与内置类型一样高效、但更安全更易用的自定义类型。类(class)是 C++ 的中心语言特性——“程序中凡是有意义的概念,都应该用一个类来表示”。
3. RAII:资源管理的基石
Resource Acquisition Is Initialization(资源获取即初始化)是 C++ 最重要的编程范式之一:
- 构造函数获取资源,析构函数释放资源
- 与异常处理天然配合:异常抛出时栈展开自动调用析构函数
- 应用范围:内存、锁、套接字、文件句柄、线程句柄……一切需要获取/释放的东西
- 智能指针(unique_ptr / shared_ptr)是 RAII 的典型应用
4. 五大特殊成员的”配套原则”
如果一个类需要自定义析构函数、拷贝或移动操作中的任何一个,它很可能需要全部五个(构造/析构/拷贝/移动/赋值)。它们是一个整体,必须配套设计。
反过来说:Rule of Zero——如果你的类不需要管理资源,就让编译器自动生成所有特殊成员。
5. 泛型编程与概念
模板提供编译期的”参数多态”和”鸭子类型”,但检查发生在实例化时,错误信息难懂。
- Concepts (C++20) 解决了这个问题:在模板接口处明确指定对类型参数的要求
- 概念不只是语法约束,更是语义要求(如”可排序""可拷贝”)
- 泛型编程的核心:算法与数据结构解耦,通过迭代器连接
6. 标准库优先原则
“Don’t reinvent the wheel; use libraries.” “When you have a choice, prefer the standard library over other libraries.”
- 标准库经过大量设计、实现和维护投入
- 标准库组件是每个完整 C++ 实现都提供的
- 用标准库写出的代码更可读、更 portable
7. 并发不是银弹
“Do not consider concurrency a panacea. If a task can be done sequentially, it is often simpler and faster to do so.”
- C++ 标准库提供的是系统级并发的基础支持(线程、互斥、原子操作、内存模型)
- 更高级的并发模型应作为库构建在这些基础之上
- 优先考虑并行算法(parallel algorithms)而非直接使用线程
- 避免数据竞争,用 RAII 管理锁
各章精华要点
第 1 章:基础(The Basics)
- C++ 是编译语言:源文件 → 编译器 → 目标文件 → 链接器 → 可执行文件
- 可移植性指源代码级别的可移植,不是二进制
- ISO 标准定义两类实体:核心语言特性 + 标准库组件
- C++ 是静态类型语言:类型在编译期确定
- 建议:聚焦编程技术,而非语言特性;不要试图掌握所有细节才能写程序
第 2 章:用户自定义类型
- 内置类型故意设计得低级,直接反映硬件能力
- C++ 的抽象机制(类、枚举)让程序员构建高层类型
- struct 是成员默认为 public 的 class
- 避免”裸”union,用类把它们和类型字段封装起来
- 优先使用 class enum 而非”普通”enum,减少意外
第 3 章:模块化
- 声明(declaration)是接口,定义(definition)是实现
- 用头文件表示接口,强调逻辑结构
- Modules (C++20) 优于头文件:更快编译、更好封装
- 用命名空间表达逻辑结构
- 异常 vs 错误码:
- 错误预期会被直接调用者处理 → 用错误码
- 错误会向上穿透多层调用 → 用异常
- 拿不准时优先用异常
- 不要在头文件里放 using-directive
第 4 章:类
- 类的三种重要形态:
- 具体类型(Concrete types):最简单,行为像 int,值语义,性能可预测
- 抽象类型(Abstract types):接口与实现完全分离,指针/引用语义
- 类层次结构(Class hierarchies):通过基类接口操作派生类对象
- 性能关键组件优先用具体类型,而非类层次
- 构造函数保证初始化;析构函数释放资源
- 只有需要直接访问类表示的函数才做成成员函数
- 对称运算符用非成员函数
第 5 章:基本操作
- 构造、析构、拷贝、移动、赋值——必须作为配套集合来设计
- Rule of Zero:不需要管理资源的类,让编译器生成全部
- Rule of Five:有资源管理需求的类,五个都要考虑
- 单参数构造函数默认声明为
explicit - 容器按值返回(依赖移动语义保证效率)
- 大操作数以 const 引用传递
- 永远不要泄漏资源
第 6 章:模板
- 模板是类型参数化的机制,支持泛型编程
- 类型安全,但检查发生在实例化时(Concepts 解决此问题)
- 函数对象(function object)和 lambda 是算法的天然搭档
- 模板没有分离编译:定义必须在每个使用它的翻译单元中可见
- 类模板参数可从构造函数推导(CTAD, C++17)
第 7 章:概念与泛型编程
- 模板的三大能力:
- 类型/值作为参数传递,不损失信息 → 内联机会大
- 实例化时融合不同上下文的信息 → 优化机会
- 常量值作为参数 → 编译期计算
- Concepts (C++20):精确指定模板接口的要求
- 作为设计工具使用
- 为所有模板参数指定概念
- 尽可能使用标准概念(如 Ranges 概念)
- 避免没有语义的”伪概念”
- 可变参数模板(Variadic Templates):处理任意数量、任意类型的参数
- 模板是编译期编程的通用机制
第 8 章:标准库总览
标准库的主要分类:
- 运行时语言支持(分配、RTTI 等)
- C 标准库(做了少量修改以增强类型安全)
- 字符串与正则表达式
- I/O 流
- 容器与算法(STL)
- 工具组件(智能指针、span、tuple 等)
- 数值计算
- 并发
- 正则表达式
- 模板元编程支持
第 9 章:字符串与正则表达式
std::string拥有字符序列,string_view只读视图- string 有移动构造,按值返回高效
- 用
string_view作为函数参数来接受多种存储方式的字符序列 - 用
at()做范围检查,用[]追求速度 s后缀用于字符串字面量(表示 std::string)- 正则表达式支持多种语法(ECMAScript 等)和多种字符类型
第 10 章:输入输出
- iostream 是类型安全、类型敏感、可扩展的
- 为有意义文本表示的自定义类型定义
<<和>> cout用于普通输出,cerr用于错误- 避免 endl(它每次都 flush,性能差),用
'\n'代替 - 流状态 fail 用于可恢复的 I/O 错误
- 支持文件流(fstream)、字符串流(stringstream)
- I/O 流类使用 RAII 管理缓冲区和文件句柄
- 文件系统库(filesystem, C++17)
第 11 章:容器
- vector 是默认容器:99% 的场景应该先选 vector
- STL 容器都是资源句柄,管理自身内存
- 序列用半开区间
[begin, end)表示 reserve()避免扩容导致的迭代器失效- 用 range-for 或算法遍历,避免手写循环越界
- 元素被拷贝进容器;要多态行为需存指针
forward_list用于通常为空的序列- 性能问题:不要相信直觉,去测量
第 12 章:算法
- STL 算法操作一个或多个序列(半开区间)
- 算法不直接修改容器大小(不增删元素)
- 搜索算法返回 end() 表示”未找到”
- 写循环时先想想能不能用标准算法表达
- 谓词(predicate)不能修改其参数
- 了解你的标准库算法,优先用它们而非手写循环
- 并行算法(C++17):利用多核的简单途径
- Ranges (C++20):容器级算法,更简洁
第 13 章:工具组件
- “小而有用”的库组件,常被称为词汇类型(vocabulary types)
- 资源管理:
unique_ptr用于独占所有权的对象(优先于 shared_ptr)shared_ptr仅用于共享所有权的场景- 用
make_unique/make_shared构造 - 资源句柄优于智能指针(特定语义)
- 移动与转发:
std::move()不要随便用std::forward()仅用于完美转发- move/forward 后不要再读对象
- span:指针+计数接口的现代替代,优先使用
- array:编译期固定大小的序列,优于内置数组
- bitset:N 位的位集合
- pair / tuple:不要过度使用,命名结构体通常更可读
第 14 章:数值计算
- C++ 不是专为数值计算设计的,但广泛用于科学/工程/金融计算
- 数值问题往往很微妙,不确定时找专家或做实验
<cmath>标准数学函数- 数值算法:
accumulate、inner_product、partial_sum、adjacent_difference std::complex复数运算- 随机数:引擎 + 分布 = 生成器;不要用 C 的 rand()
valarray用于对运行效率要求高于灵活性的数值计算numeric_limits<T>查询类型的数值属性
第 15 章:并发
- 并发用于提高吞吐量(多处理器)或提高响应性(等待时继续推进)
- 优先使用最高抽象层次:并行算法 > 任务/ future > 线程 > 原子操作
- C++ 内存模型让内存行为大致符合直觉,大多数程序员不用关心硬件细节
- 原子操作支持无锁编程,但留给专家
- 线程(
std::thread)是系统线程的类型安全接口 - 互斥量(mutex)保护共享数据:
- 用 RAII 管理锁(
scoped_lock、unique_lock) scoped_lock同时获取多个锁,避免死锁shared_lock实现读写锁- 互斥量和它保护的数据一起定义
- 用 RAII 管理锁(
- 条件变量(
condition_variable)管理线程间通信 - future / promise / packaged_task:任务间传递结果
- 避免数据竞争是并发编程第一要务
第 16 章:历史与兼容性
C++ 关键时间线:
- 1979:“C with Classes” 项目启动(Bjarne Stroustrup 在贝尔实验室)
- 1985:C++ 第一个商业版本(10月14日);《The C++ Programming Language》出版
- 1989:《The Annotated C++ Reference Manual》(ARM)
- 1998:ISO C++98 第一个国际标准
- 2003:C++03 技术修订(bug fix)
- 2011:ISO C++11 重大更新(统一初始化、lambda、移动语义、并发内存模型、auto、decltype……)
- 2014:ISO C++14(变量模板、泛型 lambda 等)
- 2017:ISO C++17(本书覆盖,结构化绑定、if constexpr、并行算法、filesystem、optional/variant/any……)
- 2020:ISO C++20(计划中:Concepts、Modules、Ranges、Coroutines……)
设计初衷:将 Simula 的程序组织能力(类、继承、虚函数)与 C 的系统编程效率和灵活性结合。
C/C++ 兼容性建议:
- C 和 C++ 的公共子集不是学习 C++ 的最佳起点
- 优先使用命名转换(
static_cast等)而非 C 风格转换 - 将 C 程序转为 C++ 时,先确保函数声明和标准头文件一致使用
- 为了可移植性和类型安全,如果必须用 C,写在 C 和 C++ 的公共子集中
C++ Core Guidelines 关联
本书的 Advice 部分大量引用并直接影响了 C++ Core Guidelines:
- 第一版 A Tour of C++ 是 Core Guidelines 的主要初始来源之一
- 每条 Advice 后标注的
[CG: X.Y]即对应 Core Guidelines 的具体条款 - 新项目或代码现代化应依赖 Core Guidelines
- 核心准则地址:
isocpp.github.io/CppCoreGuidelines/
阅读建议
- 不要纠结于每个细节:本书是”观光之旅”,建立全景感比抠语法重要
- 按编程范式理解:过程式 → 面向对象 → 泛型,而不是按语法条目记忆
- 每章的 Advice 值得细读:浓缩了 Stroustrup 几十年的工程智慧
- 配合 C++ Core Guidelines 使用:本书给全景,Core Guidelines 给具体规则
- 动手写代码验证:新特性在生产环境使用前,先写小程序测试
关联笔记
- 《结构化计算机组成》-Structured-Computer-Organization-第6版-Tanenbaum — C++ 直接映射到硬件的理论基础
- 《代码整洁之道》-Clean Code-Robert-C-Martin — 代码质量与命名风格的实践指南
- 《重构》-改善既有代码的设计 — 改善既有 C++ 代码的技术体系
- 《实现模式》-Implementation Patterns-Kent Beck — 编码决策的思维框架,与 Advice 章节互补
- 《程序设计实践》-The-Practice-of-Programming-Kernighan-Pike — C 语言传统下的工程实践,C++ 的根基
- 《算法导论》-Introduction-to-Algorithms-第3版-CLRS — STL 算法的理论基础
个人行动清单
- 用 C++17 写一个小型项目,实践现代 C++(vector、string、algorithm、smart_ptr、lambda)
- 通读 C++ Core Guidelines(先看 P 部分原则,再看 C 部分类,最后看其他)
- 学习使用 C++20 的 Concepts 和 Modules(如果编译器支持)
- 深入理解 RAII,检查自己的代码中是否有”裸”资源管理
- 用 valgrind / sanitizer 检查内存和并发问题