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 章)

章节主题核心内容
1The BasicsC++ 基础模型:编译/链接、静态类型、内置类型、指针/数组/引用、常量、作用域
2User-Defined Types抽象机制入门:struct、class、union、enum,用户自定义类型优于内置类型
3Modularity模块化:分离编译、头文件、命名空间、异常处理、Modules (C++20)
4Classes类的三种形态:具体类型、抽象类型、类层次结构;虚函数与多态
5Essential Operations五大特殊成员:构造/析构/拷贝构造/移动构造/赋值;RAII 资源管理
6Templates模板:参数化类型与操作、函数对象、lambda、模板机制
7Concepts and Generic Programming概念 (C++20)、泛型编程思想、可变参数模板、模板编译模型

第二部分:标准库(第 8-15 章)

章节主题核心内容
8Library Overview标准库总览:组件分类、头文件、命名空间
9Strings and Regular Expressionsstring、string_view、正则表达式
10Input and Outputiostream 流 I/O、格式化、文件流、字符串流、文件系统
11Containersvector、list、map、unordered_map 等 STL 容器
12AlgorithmsSTL 算法、迭代器、谓词、并行算法 (C++17)、Ranges 概念
13Utilities智能指针、span、array、bitset、tuple、时间、类型函数
14Numerics数学函数、数值算法、复数、随机数、valarray、数值极限
15Concurrency线程、互斥量、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 章:类

  • 类的三种重要形态:
    1. 具体类型(Concrete types):最简单,行为像 int,值语义,性能可预测
    2. 抽象类型(Abstract types):接口与实现完全分离,指针/引用语义
    3. 类层次结构(Class hierarchies):通过基类接口操作派生类对象
  • 性能关键组件优先用具体类型,而非类层次
  • 构造函数保证初始化;析构函数释放资源
  • 只有需要直接访问类表示的函数才做成成员函数
  • 对称运算符用非成员函数

第 5 章:基本操作

  • 构造、析构、拷贝、移动、赋值——必须作为配套集合来设计
  • Rule of Zero:不需要管理资源的类,让编译器生成全部
  • Rule of Five:有资源管理需求的类,五个都要考虑
  • 单参数构造函数默认声明为 explicit
  • 容器按值返回(依赖移动语义保证效率)
  • 大操作数以 const 引用传递
  • 永远不要泄漏资源

第 6 章:模板

  • 模板是类型参数化的机制,支持泛型编程
  • 类型安全,但检查发生在实例化时(Concepts 解决此问题)
  • 函数对象(function object)和 lambda 是算法的天然搭档
  • 模板没有分离编译:定义必须在每个使用它的翻译单元中可见
  • 类模板参数可从构造函数推导(CTAD, C++17)

第 7 章:概念与泛型编程

  • 模板的三大能力:
    1. 类型/值作为参数传递,不损失信息 → 内联机会大
    2. 实例化时融合不同上下文的信息 → 优化机会
    3. 常量值作为参数 → 编译期计算
  • Concepts (C++20):精确指定模板接口的要求
    • 作为设计工具使用
    • 为所有模板参数指定概念
    • 尽可能使用标准概念(如 Ranges 概念)
    • 避免没有语义的”伪概念”
  • 可变参数模板(Variadic Templates):处理任意数量、任意类型的参数
  • 模板是编译期编程的通用机制

第 8 章:标准库总览

标准库的主要分类:

  1. 运行时语言支持(分配、RTTI 等)
  2. C 标准库(做了少量修改以增强类型安全)
  3. 字符串与正则表达式
  4. I/O 流
  5. 容器与算法(STL)
  6. 工具组件(智能指针、span、tuple 等)
  7. 数值计算
  8. 并发
  9. 正则表达式
  10. 模板元编程支持

第 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 实现读写锁
    • 互斥量和它保护的数据一起定义
  • 条件变量(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/

阅读建议

  1. 不要纠结于每个细节:本书是”观光之旅”,建立全景感比抠语法重要
  2. 按编程范式理解:过程式 → 面向对象 → 泛型,而不是按语法条目记忆
  3. 每章的 Advice 值得细读:浓缩了 Stroustrup 几十年的工程智慧
  4. 配合 C++ Core Guidelines 使用:本书给全景,Core Guidelines 给具体规则
  5. 动手写代码验证:新特性在生产环境使用前,先写小程序测试

关联笔记


个人行动清单

  • 用 C++17 写一个小型项目,实践现代 C++(vector、string、algorithm、smart_ptr、lambda)
  • 通读 C++ Core Guidelines(先看 P 部分原则,再看 C 部分类,最后看其他)
  • 学习使用 C++20 的 Concepts 和 Modules(如果编译器支持)
  • 深入理解 RAII,检查自己的代码中是否有”裸”资源管理
  • 用 valgrind / sanitizer 检查内存和并发问题