本文摘取 learncpp 教程中的规则最佳实践警告部分构成(由笔者手动复制粘贴)。像所有最佳实践一样,本文内容应作为指导方针,而不是绝对的教条。在遵循它们的同时,请始终结合具体场景进行判断。

全文共 218 条规则,按 31 个主题分组整理。点击各主题标题即可展开查看,文末附有核心原则速览表。

1. 变量与初始化

良好的变量习惯是写出清晰、可维护 C++ 代码的基础。

  • #1 首选直接列表初始化值初始化来初始化变量,并在创建变量时立即初始化。
  • #2 将局部变量定义在尽可能靠近其首次使用的地方。
  • #3 不要对值参数使用 const,按值返回时也不要使用 const。除此之外,应尽可能将变量声明为常量。优先使用常量变量,而不是带有替换文本的类对象宏。
  • #4 避免在代码中使用“魔法数字”,而是使用 constexpr 变量。
  • #5 任何初始化器为常量表达式的常量变量,都应声明为 constexpr;初始化器不是常量表达式的,则应声明为 const
  • #6 尽可能使用局部变量而非全局变量。
  • #7 只在循环内部使用的变量,应该在循环的作用域内部定义。
2. 类型与数值

选择合适的类型和字面量,可以避免许多隐式转换和精度问题。

  • #8 对于存储数量(即使是非负数量)和数学运算,优先使用有符号数而不是无符号数。避免混合使用有符号和无符号数。
  • #9 始终确保字面量的类型与它们被赋值或用于初始化的变量类型匹配,否则可能导致不必要的转换甚至精度损失。
  • #10 除非空间非常宝贵,否则优先使用 double 而不是 float,因为 float 的精度不足通常会导致不准确。
  • #11 首选字面量后缀 L(大写),而不是 l(小写)。
  • #12 单个字符通常应使用单引号(例如 't''\n'),而不是双引号。一个例外是在输出时,为保持一致性可统一使用双引号。避免多字符字面量(例如 '56')。
  • #13 尽可能避免对小于 int 的整数类型进行位移。
  • #14 整数循环变量通常应为有符号整数类型。
3. 类型别名与类型推导

类型别名和类型推导能让代码更易读、更易维护,但要有节制地使用。

  • #15 将类型别名以大写字母开头命名,并且不要使用后缀(除非有特殊原因)。
  • #16 优先使用类型别名(using)而不是 typedef
  • #17 明智地使用类型别名——当它们能为代码可读性或代码维护带来明显好处时。
  • #18 当对象的类型无关紧要时,为变量使用类型推导(auto)。
  • #19 当需要一个与初始化器类型不同的特定类型,或对象处于使类型明确有用的上下文中时,优先使用显式类型。
4. 类型转换

显式、受控的转换可以避免许多隐蔽的错误。

  • #20 窄化转换可能不安全且是错误来源,应尽可能避免。如果确实需要执行窄化转换,请使用 static_cast 将其变为显式转换。
  • #21 除非有充分理由,否则避免使用 const_castreinterpret_cast。需要将值从一种类型转换为另一种类型时,优先使用 static_cast(也优先于初始化临时对象)。
  • #22 避免使用 C 风格强制类型转换。
5. 头文件与包含

头文件管理直接影响编译效率和模块化质量。

  • #23 不要在头文件中放置函数和变量定义。例外包括:内联函数、内联变量、类型和模板的定义。
  • #24 源文件应 #include 其配对的头文件(如果存在)。避免 #include .cpp 文件。每个文件都应显式 #include 所有编译所需头文件,不要依赖通过其他头文件传递包含的头文件。
  • #25 为最大限度提高编译器标记缺失包含的可能性,请按以下顺序排列 #include
    1. 此代码文件的配对头文件(例如 add.cpp#include "add.h");
    2. 同一项目的其他头文件(例如 #include "mymath.h");
    3. 第三方库头文件(例如 #include <boost/tuple/tuple.hpp>);
    4. 标准库头文件(例如 #include <iostream>)。
      每个分组内部应按字母顺序排序(除非第三方库文档另有说明)。
6. 函数与作用域

函数设计应追求清晰、低嵌套和明确的作用域。

  • #26 避免在单个语句中定义多个相同类型的变量。相反,在单独语句中每行定义一个变量,并用单行注释记录其用途。
  • #27 遵循 DRY 原则:“不要重复自己”。如需多次做某事,考虑用变量、函数或循环消除冗余。
  • #28 当函数参数存在但未在函数体中使用时,不要为其命名。你可以选择只在注释中放置一个名称。
  • #29 当函数内需要一个变量时:若调用者会通过实参传入初始值,使用函数形参;否则使用局部变量。
  • #30 使用显式命名空间前缀访问命名空间中定义的标识符。
  • #31 宏名称应以大写字母书写,单词之间用下划线分隔。除非没有可行的替代方案,否则避免使用带有替换文本的宏。
  • #32 优先使用 ++x 版本,因为它们更高效,也更不容易引起意外。
  • #33 将函数的嵌套级别保持在 3 层或更少。如果需要更多嵌套,考虑将函数重构为子函数。
  • #34 在最受限的现有作用域中定义变量。避免创建唯一目的是限制变量作用域的新块。优先在命名空间内定义全局变量,而不是在全局命名空间中。
  • #35 除非有特定、令人信服的理由(例如正在头文件中定义这些函数或变量),否则请避免使用 inline 关键字。
  • #36 仅当无法安全或合理地从 main 函数正常返回时才使用 std::exit。如果尚未禁用异常,优先使用异常来安全地处理错误。
  • #37 优先使用显式返回类型而不是返回类型推导(除非返回类型不重要、难以表达或脆弱)。
  • #38 使用函数重载使程序更简单。
  • #39 如果函数有前向声明(尤其是头文件中的前向声明),请将默认参数放在那里;否则,将默认参数放在函数定义中。
7. 字符串与输入输出

正确处理字符串和 I/O 对性能和正确性都至关重要。

  • #40 当一行输出完成后,输出一个换行符。向控制台输出文本时,首选 \n 而不是 std::endl
  • #41 如果使用 std::getline() 读取字符串,请使用 std::cin >> std::ws 输入操纵符来忽略前导空格。由于 std::ws 不会在调用之间保留,每个 std::getline() 调用都需要单独处理。
  • #42 不要按值传递 std::string,因为它会产生昂贵的复制。当你需要只读字符串时,尤其是在函数参数中,优先使用 std::string_view 而不是 std::string
  • #43 不要使用 std::string 字面量初始化 std::string_view,因为这会导致 std::string_view 悬空。使用 C 风格字符串字面量或 std::string_view 字面量是安全的;只要底层字符串对象的生命周期长于视图,使用 C 风格字符串对象、std::string 对象或 std::string_view 对象初始化也安全。
  • #44 如果参数是一个在包含函数调用的完整表达式结束时销毁的临时变量,返回的 std::string_view 必须立即使用,否则临时变量销毁后将悬空。
  • #45 请注意不要编写任何假定 std::string_view 以空字符结尾的代码。
  • #46 传递字符串时首选 std::string_view(按值),而不是 const std::string&,除非函数需要调用其他接受 C 风格字符串或 std::string 参数的函数。
  • #47 不要将内存地址写入文件。当从磁盘读回这些值时,原来位于这些地址的变量可能已位于不同的地址,这些地址将无效。
8. 表达式与运算符

谨慎编写表达式,避免未定义行为和可读性陷阱。

  • #48 确保你编写的表达式(或函数调用)不依赖于操作数(或参数)的求值顺序。
  • #49 如果可能,最好将余数运算符 operator% 的结果与 0 进行比较。
  • #50 C++ 不定义函数参数或运算符操作数的求值顺序。在给定语句中,不要多次使用已应用副作用的变量,否则结果可能未定义。
  • #51 当条件运算符在复合表达式中使用时,将整个条件操作(包括操作数)加括号。若条件包含任何运算符(函数调用运算符除外),考虑为其加括号。复杂表达式中尽量避免使用条件运算符。
  • #52 不要在条件中添加不必要的 ==!=。如果浮点值可能经过计算,避免使用 ==!= 来比较它们。
  • #53 短路求值可能导致逻辑与和逻辑或不评估右操作数。避免将带有副作用的表达式与这些运算符结合使用。
  • #54 在单个表达式中混合使用逻辑与和逻辑或时,请明确地将每个操作用括号括起来,以确保它们按照你期望的方式求值。
  • #55 位操作是少数应明确使用无符号整数(或 std::bitset)的场景之一。为避免意外,请对无符号整数操作数或 std::bitset 使用位运算符。
9. 控制流

清晰的控制流能显著降低代码的认知负担。

  • #56 初始化你的静态局部变量。静态局部变量只在代码首次执行时初始化,而不是后续调用时。const 静态局部变量通常可以安全使用;非 const 静态局部变量通常应避免使用。如果确实使用,请确保该变量永远不需要重置,并且不用于改变程序流程。
  • #57 优先使用显式命名空间限定符,而不是 using 语句。完全避免使用 using-directives(除了 using namespace std::literals 来访问 ssv 字面量后缀)。using-declarations 可以在 .cpp 文件中、在 #include 指令之后使用。不要在头文件中使用 using 语句,尤其不要在头文件的全局命名空间中。
  • #58 当条件是常量表达式时,优先使用 constexpr if 语句而不是非 constexpr if 语句。
  • #59 标签下的每组语句都应该以 break 语句或 return 语句结束,包括 switch 中最后一个标签下的语句。
  • #60 尽量不要缩进标签,使它们能够从周围代码中脱颖而出,而不会暗示它们定义了嵌套作用域。
  • #61 当针对少量值测试单个表达式(具有非布尔整数类型或枚举类型)的相等性时,优先使用 switch 语句而不是 if-else 语句。
  • #62 使用 [[fallthrough]] 属性(连同空语句)来表示有意的穿透。
  • #63 如果在 case 语句中定义变量,请在 case 内的块中进行。
  • #64 避免使用 goto 语句,除非替代方案对代码可读性的影响显著更差。
  • #65 对于有意无限循环,请优先使用 while(true)
  • #66 在选择相等时,优先选择 while 循环而不是 do-while 循环。
  • #67 当存在明显的循环变量时,优先选择 for 循环而不是 while 循环。当没有明显的循环变量时,优先选择 while 循环而不是 for 循环。
  • #68 通常优先选择迭代而不是递归,除非递归确实有意义。
10. 全局变量与链接

全局状态应尽量少用,且必须管理其可见性。

  • #69 考虑在使用全局变量命名时(特别是那些在全局命名空间中定义的变量)使用 gg_ 前缀,以帮助将它们与局部变量和函数参数区分开来。
  • #70 当你有明确理由不允许其他文件访问时,为标识符提供内部链接。考虑为你不想被其他文件访问的所有标识符提供内部链接(为此使用匿名命名空间)。
  • #71 如果你想定义一个未初始化的非 const 全局变量,请不要使用 extern 关键字,否则 C++ 会认为你正在尝试为该变量进行前向声明。尽管可以通过 extern 关键字赋予 constexpr 变量外部链接,但它们不能被前向声明为 constexpr,因为编译器需要在编译时知道其值。仅将 extern 用于全局变量前向声明或 const 全局变量定义。不要将 extern 用于非 const 全局变量定义(它们隐式为 extern)。
  • #72 如果你需要全局常量并且编译器支持 C++17,请优先在头文件中定义内联 constexpr 全局变量。
11. 命名空间

命名空间是组织代码和避免命名冲突的重要工具。

  • #73 当你有要保留在翻译单元本地的内容时,首选匿名命名空间。避免在头文件中使用匿名命名空间。
12. 随机数

随机数生成需要注意种子和重新播种的策略。

  • #74 使用 std::random_device 为你的 PRNG 播种(除非它在目标编译器/架构上没有正确实现)。只播种给定伪随机数生成器一次,不要重新播种。
13. 测试与调试

把程序切分成可测试的小单元,并让静态断言尽可能在编译期捕获错误。

  • #75 将程序编写成小的、定义良好的单元(函数或类),经常编译,并随时测试代码。
  • #76 力争代码达到 100% 的分支覆盖率。使用 0、1、2 测试来确保循环在不同迭代次数下正常工作,并测试不同类别的输入值以确保单元正确处理它们。
  • #77 尽可能优先使用 static_assert 而不是 assert()
14. 模板

模板是泛型编程的核心,命名与定义位置都有约定;函数模板特化应尽量避免。

  • #78 对于以简单或显而易见的方式使用、表示“任何合理类型”的类型模板参数,使用单个大写字母(如 TUV)命名;如果用法不明显或必须满足特定要求,则使用更具描述性的名称(如 AllocatorTAllocator)。
  • #79 调用从函数模板实例化的函数时,优先使用普通函数调用语法(除非需要函数模板版本优先于匹配的非模板函数)。
  • #80 在需要时,使用函数模板编写可处理各种类型的泛型代码。
  • #81 可以放心使用只有一个 auto 参数的简写函数模板,或每个 auto 参数都是独立类型的情况(语言标准需设置为 C++20 或更高版本)。
  • #82N 用作 int 非类型模板参数的名称。
  • #83 需要在多个文件中使用的模板应在头文件中定义,然后在使用它们的地方 #include,以便编译器看到完整的模板定义并在需要时实例化。
  • #84 完全特化不是隐式内联的(部分特化是隐式内联的)。如果将完全特化放在头文件中,应标记为 inline,以免被包含到多个翻译单元时违反 ODR。
  • #85 通常,应尽可能避免函数模板特化,而倾向于使用非模板函数。
15. constexpr 与 consteval 函数

编译期求值能力需要在正确的上下文中验证。

  • #86 所有 constexpr 函数都应能在编译时求值。始终在需要常量表达式的上下文中测试 constexpr 函数——运行时求值有效的函数,编译时求值可能失败。
  • #87 仅在单个源文件(.cpp)中使用的 constexpr/consteval 函数应在该源文件中使用点的上方定义;在多个源文件中使用的应在头文件中定义,以便包含到每个源文件中。
  • #88 如果函数出于某种原因必须在编译时求值(例如执行只有在编译时才能完成的操作),请使用 consteval
  • #89 除非有特定理由,可作为常量表达式一部分求值的函数应声明为 constexpr(即使目前未以这种方式使用);不能作为所需常量表达式求值的函数不应标记为 constexpr
16. 引用与指针

引用应作为默认选择,指针只在其附加能力不可或缺时使用。

  • #90 定义引用时,将 & 放在类型旁边(而不是引用变量的名称旁边)。
  • #91 除非需要修改被引用的对象,否则优先使用 const 左值引用,而不是非 const 的左值引用。
  • #92 优先使用 const 引用传递而不是非 const 引用传递,除非有特殊原因(例如函数需要更改实参的值)。
  • #93 经验法则:基本类型按值传递,类类型按 const 引用传递。不确定时按 const 引用传递,因为这样最不容易遇到意外行为。
  • #94 声明指针类型时,将 * 放在类型名称旁边,并始终初始化指针。
  • #95 如果不用有效对象的地址初始化指针,请对其进行值初始化(使其成为空指针)。需要空指针字面值进行初始化、赋值或传参时,使用 nullptr
  • #96 指针应要么保存有效对象的地址,要么为 nullptr——这样只需测试指针是否为空,即可假定任何非空指针都是有效的。
  • #97 除非需要指针提供的附加功能,否则优先使用引用而不是指针。
  • #98 优先使用指向 const 的函数参数,而不是指向非 const 的(除非函数需要修改传入的对象);除非有特定原因,不要将函数参数设为 const 指针。
  • #99 除非有特定原因,优先按引用传递而非按地址传递。
  • #100 想要 const 引用时,即使没有严格必要,也要重新应用 const 限定符——它使意图清晰并有助于防止错误。
  • #101 想要 const 指针、指向 const 的指针或指向 constconst 指针时,同样重新应用 const 限定符。
  • #102 将已删除的指针设置为 nullptr,除非它们会立即超出作用域。
  • #103 删除空指针是允许的,且没有任何效果——不需要对 delete 语句进行条件判断。
17. 返回值与可选值

返回值的生命周期和“无值”表达方式是接口设计的关键。

  • #104 引用生命周期延长不跨函数边界工作。
  • #105 避免返回对非 const 局部静态变量的引用。
  • #106 除非返回“无对象”(使用 nullptr)的能力很重要,否则优先按引用返回而不是按地址返回。
  • #107 避免使用输出参数(极少数没有更好选择的情况除外)。对于非可选的输出参数,首选按引用传递。
  • #108 对于可能失败的函数,返回 std::optional(而不是哨兵值),除非函数需要返回有关其失败原因的附加信息。
  • #109 可选返回类型首选 std::optional;可选函数参数首选函数重载。否则,当 T 通常按值传递时使用 std::optional<T>,当 T 复制成本高时优先 const T*
18. 程序定义类型与枚举

自定义类型和枚举应有统一的命名与组织约定。

  • #110 程序定义类型应以大写字母开头命名,并且不使用后缀。
  • #111 仅在一个代码文件中使用的程序定义类型应在该文件中、尽可能靠近首次使用点定义;在多个代码文件中使用的应定义在与类型同名的头文件中,再按需 #include 到每个代码文件中。
  • #112 枚举类型名以大写字母开头,枚举器名以小写字母开头。
  • #113 优先将枚举放在命名作用域区域(例如命名空间或类)内,避免枚举器污染全局命名空间。
  • #114 除非有令人信服的理由,避免为枚举器分配显式值。
  • #115 仅在必要时指定枚举的基类型。
  • #116 优先使用作用域枚举(enum class)而不是无作用域枚举,除非有充分理由。
19. 聚合与结构体

聚合应保持简单:只持有数据,把初始化安全交给默认值和值初始化。

  • #117 初始化聚合时,首选(非复制)花括号列表形式。
  • #118 向聚合添加新成员时,最安全的方法是将其添加到定义列表的底部,避免其他成员的初始化器移位。
  • #119 为所有成员提供默认值——即使变量定义不包含初始化列表,成员也会被初始化。
  • #120 对于聚合,首选值初始化(空大括号 {})而不是默认初始化(不使用大括号)。
  • #121 结构体(和类)通常应是所有者:确保每个数据成员都具有所有权类型(而不是视图、指针或引用)。
  • #122 使用指针访问成员时,使用成员选择运算符 -> 而不是 .
  • #123 通常最好避免在默认成员初始化器中使用其他成员。
  • #124 成员函数可以与结构体和类一起使用;但结构体应避免定义构造函数,因为这会使其成为非聚合体。
20. 类与成员

类的设计应突出公共接口,弱化实现细节。

  • #125 如果类类型没有数据成员,优先使用命名空间。
  • #126 不(也永远不会)修改对象状态的成员函数应设为 const,以便能在 const 和非 const 对象上调用。
  • #127 考虑将 private 数据成员以 m_ 前缀命名,以便与局部变量、函数参数和成员函数区分;类的 public 成员也可遵循此约定,但结构体的 public 成员通常不使用。
  • #128 类通常应将成员变量设为 private(或 protected)、成员函数设为 public;结构体通常避免使用访问说明符(所有成员默认 public)。
  • #129 返回引用的成员函数应返回与数据成员相同类型的引用,以避免不必要的转换。
  • #130 右值对象在其创建的完整表达式结束时被销毁,此时对其成员的任何引用都会悬空——对右值对象成员的引用只能在创建该右值对象的完整表达式中安全使用。
  • #131 优先立即使用按引用返回的成员函数的返回值,避免隐式对象是右值时出现悬空引用。
  • #132 尽可能优先将函数实现为非成员函数(特别是包含应用程序特定数据或逻辑的函数)。
  • #133 先声明公共成员,其次是受保护成员,最后是私有成员——突出公共接口,弱化实现细节。
  • #134 最好将类定义放在与类同名的头文件中;琐碎的成员函数(如访问函数、空函数体的构造函数)可在类定义内定义;非琐碎的成员函数最好在与类同名的源文件中定义。
  • #135 成员函数的默认参数放在类定义内部。
  • #136 在类定义的顶部定义嵌套类型。
  • #137 在类定义之外定义的成员函数模板,应紧邻类定义下方(同一文件中)定义。
  • #138 使用类名和作用域解析运算符 :: 访问静态成员。
  • #139 将静态成员声明为 inlineconstexpr,以便在类定义中初始化。
  • #140 友元函数应尽可能优先使用类接口,而不是直接访问成员。
  • #141 尽可能且合理地将函数实现为非友元。
21. 构造函数与成员初始化

构造函数的正确写法决定了对象诞生时的状态是否可靠。

  • #142 成员初始化列表中的成员变量应按它们在类中定义的顺序列出。
  • #143 优先使用成员初始化列表来初始化成员,而不是在构造函数体中赋值。
  • #144 对于所有类类型,优先使用值初始化而不是默认初始化。
  • #145 优先使用显式默认构造函数(= default)而不是空函数体的默认构造函数。
  • #146 构造函数不应从另一个函数体中直接调用——这会导致编译错误,或直接初始化一个临时对象。确实想要临时对象时,优先使用列表初始化(清楚表明你打算创建一个对象)。
  • #147 如果有多个构造函数,考虑是否可以使用委托构造函数来减少重复代码。
  • #148 用户必须提供初始化值的成员应先定义(并作为构造函数最左边的参数);可选提供初始化值的成员(默认值可接受)后定义(最右边的参数)。
  • #149 快速经验法则:转换为基本类型时首选 static_cast,转换为类类型时首选列表初始化的临时对象。
  • #150 以下情况首选 static_cast 创建临时对象:需要执行窄化转换;想明确表明正在转换为行为不同的类型(如 charint);想使用直接初始化(如避免列表构造函数优先)。以下情况首选列表初始化创建新对象:想防止窄化转换或需要调用列表构造函数;需要向构造函数提供额外参数以促进转换。
  • #151 默认将任何接受单个参数的构造函数声明为 explicit;若类型间的隐式转换在语义上等价且性能良好,可例外。不要将拷贝或移动构造函数声明为 explicit,因为它们不执行转换。
22. 拷贝与移动(三五法则)

特殊成员函数的成组规则是资源管理类设计的核心。

  • #152 拷贝构造函数除复制外不应有副作用;优先使用隐式拷贝构造函数;如果自己编写,参数应为 const 左值引用。
  • #153 三法则:如果一个类需要用户定义的析构函数、拷贝构造函数或拷贝赋值运算符之一,那么它很可能三个都需要。
  • #154 隐式移动构造函数和移动赋值会复制指针而不是移动它们——如果想移动指针成员,需要自己定义移动构造函数和移动赋值。
  • #155 五法则:如果定义或删除了拷贝构造函数、拷贝赋值、移动构造函数、移动赋值或析构函数中的任何一个,则所有这些函数都应定义或删除。
23. 数组与容器

优先使用标准容器和基于范围的循环,远离 C 风格数组。

  • #156 初始化具有列表构造函数的容器时:初始化器是元素值(要调用列表构造函数)就使用大括号初始化;初始化器不是元素值(要调用非列表构造函数)就使用直接初始化。
  • #157 如果提供了列表构造,最好也提供列表赋值。
  • #158 对于支持移动的类型,倾向按 const 引用传递,并按值返回。
  • #159 尽可能避免使用整型值进行数组索引。
  • #160 遍历容器时,优先使用基于范围的 for 循环而不是常规 for 循环,并配合类型推导(auto)让编译器推导元素类型。
  • #161 基于范围的 for 循环的元素类型:要修改元素的副本用 auto;要修改原始元素用 auto&;只需查看原始元素用 const auto&
  • #162 使用 static_assert 确保 constexpr 数组的长度与计数枚举器匹配;非 constexpr 数组使用 assert
  • #163 创建新的临时对象添加到容器中、或需要访问显式构造函数时,首选 emplace_back();否则首选 push_back()
  • #164 优先选择 constexpr std::bitsetstd::vector<char> 或第三方动态位集,而不是 std::vector<bool>
  • #165 constexpr 数组使用 std::array,非 constexpr 数组使用 std::vector
  • #166 尽可能将 std::array 定义为 constexpr;如果做不到,考虑改用 std::vector
  • #167 使用类模板参数推导(CTAD)让编译器从初始化器中推导 std::array 的类型和长度。
  • #168 明确地用一个值初始化每个数组元素时,最好省略 C 风格数组的长度。
  • #169 期望 C 风格数组类型的函数参数应使用数组语法(如 int arr[]),而不是指针语法(如 int *arr)。
  • #170 实际可行时避免 C 风格数组:只读字符串首选 std::string_view;可修改的字符串首选 std::string;非全局 constexpr 数组首选 std::array;非 constexpr 数组首选 std::vector;全局 constexpr 数组可以使用 C 风格数组。
  • #171 从数组开头(元素 0)开始索引时倾向使用下标,使索引与元素对齐;从给定元素进行相对定位时倾向使用指针算术。
  • #172 避免非 const C 风格字符串对象,优先使用 std::string
  • #173 避免 C 风格字符串符号常量,改用 constexpr std::string_view
24. 算法

标准算法库经过充分优化与测试,是手写循环的首选替代。

  • #174 在使用特定算法之前,确保其性能和执行顺序保证适用于你的特定用例。
  • #175 优先使用算法库中的函数,而不是自己编写相同功能的代码。
25. Lambda 表达式

lambda 让一次性函数就地定义,但捕获方式与可变性需要克制。

  • #176 遵循在最小作用域中定义事物、最接近首次使用的最佳实践:当需要一个简单的一次性函数作为参数传递给其他函数时,lambda 优于普通函数。
  • #177 将 lambda 存储在变量中时,使用 auto 作为变量类型。将 lambda 传递给函数时:支持 C++20 就用 auto 作为参数类型;否则使用带类型模板参数的函数或 std::function 参数(lambda 没有捕获时可使用函数指针)。
  • #178 仅当变量的值很短且类型明确时才在捕获中初始化变量;否则最好在 lambda 外部定义变量并捕获它。
  • #179 尽量避免使用可变 lambda——不可变 lambda 更易于理解,也不会出现并行执行时更危险的问题。
26. 运算符重载与类型转换运算符

重载的运算符应直观、符合原始意图。

  • #180 重载运算符应至少操作一个程序定义类型(作为函数的参数或隐式对象)。
  • #181 重载运算符时,最好使运算符的功能尽可能接近运算符的原始意图。
  • #182 如果重载运算符的含义不明确或不直观,改用命名函数。
  • #183 不修改其操作数的运算符(如算术运算符)通常按值返回结果;修改其最左操作数的运算符(如前置 ++、任何赋值运算符)通常按引用返回最左操作数。
  • #184 如果可以在不添加额外函数的情况下实现,优先将运算符重载为普通函数而不是友元函数。
  • #185 形式选择经验法则:重载赋值 =、下标 []、函数调用 () 或成员选择 -> 时作为成员函数;重载一元运算符时作为成员函数;重载不修改左操作数的二元运算符(如 operator+)时作为普通函数(首选)或友元函数;重载修改左操作数、但无法向其类定义添加成员的(如左操作数为 ostreamoperator<<)作为普通函数(首选)或友元函数;重载修改左操作数且可以修改其定义的(如 operator+=)作为成员函数。
  • #186 只定义对你的类有直观意义的重载运算符。
  • #187 确保不要试图在指向对象的指针上调用重载的 operator[]
  • #188 就像单参数转换构造函数应标记为 explicit 一样,类型转换运算符也应标记为 explicit,除非要转换的类型与目标类型本质上是同义的。
  • #189 可能的话,优先使用转换构造函数,避免使用重载类型转换。
  • #190 需要定义类型 A 到类型 B 的转换时:如果 B 是可修改的类类型,优先用转换构造函数从 A 创建 B;否则如果 A 是可修改的类类型,用重载类型转换将 A 转换为 B;再否则,使用非成员函数。
27. 智能指针

智能指针用 RAII 接管动态内存的生命周期,是异常安全代码的基石。

  • #191 优先使用 std::arraystd::vectorstd::string,而不是用智能指针管理固定大小数组、动态数组或 C 风格字符串。
  • #192 使用 std::make_unique(),而不是自己创建 std::unique_ptr 并使用 new
  • #193 如果需要多个 std::shared_ptr 指向同一个资源,始终复制一个现有的 std::shared_ptr
28. 组合与聚合

对象间的关系建模应服务于程序需求,而非对现实世界的刻板模仿。

  • #194 实现满足程序需求的最简单关系类型,而不是在现实生活中看起来正确的关系类型。
  • #195 应优先使用组合而非聚合。
29. 继承与多态

继承体系中的虚函数与析构函数需要格外小心。

  • #196 优先使用 private 成员而不是 protected 成员。
  • #197 除非有特殊原因,使用公有继承。
  • #198 避免多重继承,除非替代方案会导致更高的复杂性。
  • #199 如果一个函数是虚函数,则派生类中所有匹配的重写都隐式是虚函数。
  • #200 切勿从构造函数或析构函数中调用虚函数。
  • #201 在基类的虚函数上使用 virtual 关键字;在派生类的覆盖函数上使用 override 说明符(不再使用 virtual 关键字)——包括虚析构函数。
  • #202 成员函数同时是 constoverride 时,const 必须列在前面:const override 正确,override const 错误。
  • #203 处理继承时,任何显式析构函数都应设为虚函数。
  • #204 任何带有纯虚函数的类也应该有一个虚析构函数。
  • #205 打算让类被继承:确保析构函数是虚函数且公有;不打算让类被继承:将类标记为 final,阻止其他类继承它,且不对类本身施加其他使用限制。
  • #206 始终通过检查空指针结果来确保 dynamic_cast 动态转换确实成功。
30. 异常

先判断异常是否真的合适;一旦使用,就要保证析构不抛异常、移动操作不失败,并在 main 中准备收尾。

  • #207 当以下所有条件都满足时,异常处理是最好的选择:所处理的错误极少发生;错误很严重,否则无法继续执行;错误无法在其发生的地方处理;没有其他好的方法可以将错误代码返回给调用者。
  • #208 如果异常未处理,调用堆栈可能展开也可能不展开;如果未展开,局部变量不会被销毁——若这些变量有非平凡的析构函数,可能导致问题。
  • #209 如果你的程序使用异常,请考虑在 main 中使用万能处理程序(catch(...)),以帮助确保在发生未处理异常时行为有序。如果异常被万能处理程序捕获,你应该假定程序现在处于某种不确定状态,立即执行清理,然后终止。
  • #210 基本类型的异常可以按值捕获(复制成本低);类类型的异常应按(const)引用捕获,防止昂贵的复制和切片。
  • #211 派生异常类的处理程序应列在基类处理程序之前。
  • #212 重新抛出相同的异常时,单独使用 throw 关键字。
  • #213 需要构造函数处理成员初始化列表中抛出的异常时,使用函数级 try 块。
  • #214 避免让控制流到达函数级 catch 块的末尾——显式地抛出、重新抛出或返回。
  • #215 如果在堆栈展开期间从析构函数抛出异常,程序将终止。
  • #216 始终将移动构造函数、移动赋值和交换函数设为 noexcept;尽可能将拷贝构造函数和拷贝赋值运算符也设为 noexcept;在其他函数上使用 noexcept 表达不失败或不抛出保证。
  • #217 如果不确定函数是否应具有不失败/不抛出保证,谨慎起见不标记 noexcept——撤销 noexcept 会违反对用户做出的接口承诺并可能破坏现有代码,而事后为非 noexcept 函数添加 noexcept 则是安全的。
  • #218 如果某个类型既有潜在抛出的移动语义、又删除了拷贝语义(拷贝构造和拷贝赋值不可用),std::move_if_noexcept 将放弃强保证并调用移动语义——标准库容器类中无处不在这种条件性放弃,因为它们经常使用 std::move_if_noexcept

核心原则速览

原则 核心要点
立即初始化 变量创建时就初始化,优先列表初始化
显式优于隐式 命名空间限定、const/constexprexplicit、显式链接
不要重复自己 用函数、变量、循环和委托构造消除冗余
控制作用域 局部优先、最小作用域、谨慎使用全局变量
安全第一 避免未定义行为、悬空视图、不当的 extern 与窄化转换
三五法则 特殊成员函数要么全隐式,要么成组定义或删除
资源管理 优先容器、std::string 与智能指针,少用裸 new/delete
异常安全 慎用异常;移动 noexcept;析构不抛;main 中万能处理程序收尾

将这些实践融入日常编码,会让你的 C++ 代码更健壮、更易读,也更容易维护。