C++编程规范:编程风格

第14条:宁要编译时和连接时错误,也不要运行时错误

  1. 编写代码时,应该在编译期间使用编译器检查不变式,而不应该在运行时再进行检查
  2. 静态检查与数据和控制流无关。静态检查能够提供独立于程序输入和执行流程的保证。相反,要确保运行时检查足够可靠,需要使用对所有输入都具有代表性的用例进行测试
  3. 静态表示的模型更加可靠
  4. 静态检查不会带来运行时开销
  5. 可以使用编译时检查代替运行时检查的情况:
    • 编译时布尔条件,如果测试的是编译时的布尔条件,那么可以使用静态断言取代运行时测试
    • 编译时多态,在定义泛型函数或者类型时,考虑用编译时多态(模板)代替运行时多态(虚函数)
    • 枚举,在需要使用符号常量或首先整数值时考虑定义enum
    • 向下强制:如果经常使用dynamic_cast执行向下强制,说明基类提供的功能太少了,此时可以考虑重新设计接口,使程序能够用基类表示计算。
  6. 例外:有些情况下,无法在编译时检查,必须进行运行时检查,对于这些情况,应该使用断言来检查内部编程错误。

    第15条:积极使用const

  7. 使用mutable成员实现逻辑上的不变,当类的const成员函数需要合法地修改成员变量时,声明该成员变量为mutable的。如果使用Piml惯用法隐藏了所有私有成员,就无需对缓存信息或指向它的为改变的指针声明mutable了。
  8. 在函数声明中,要避免将通过值传递的函数参数声明为const,但是这种顶级const将对函数的定义产生影响,能够敏感地捕获对参数的无意改变。 void Fun(int x); void Fun(const int x); //重新声明同一函数:顶级const将被忽略

    第16条:避免使用宏

  9. 关于宏的第一条规则:不要使用它,除非不得不用,几乎每个宏都说明程序设计语言、程序或者程序员存在缺陷
  10. 宏可能展开为在偶然情况下能够编译的“传输线噪声”,宏中的错误可能只有在宏展开之后才能被报告出来,而不是在定义时
  11. 不要考虑编写一个以常见词或缩略语为名字的宏
  12. 尽可能快地取消宏的定义(#undef)
  13. 不要讲宏放在头文件中
  14. 在条件编译中,要避免在代码中导出杂乱地插入#ifdef

    第17条:避免使用“魔数”

  15. 避免在代码中直接使用文字常量
  16. 名称能够增加信息,并提供单一的维护点,而程序中到处重复的原始数据时无名的,维护起来很麻烦。
  17. 常量应该是枚举符或者const值,有合适的作用域和名称
  18. 可以在类定义中定义静态整数常量,其他类型的常量需要单独的定义或者一个短小的函数

    第18条:尽可能局部地声明变量

  19. 变量的生存期超过必需的长度时会产生以下几个缺点:会使程序更难以理解和维护;名字会污染上下文;不能总是被合理地初始化
  20. 在能够合理地初始化一个变量之前,绝不要声明它
  21. 尽可能局部地定义每个变量,通常就是在有了足够的数据进行初始化的时候,而且恰恰就在首次使用变量之前。
  22. 例外情况:
    • 有时候将变量提出循环是有好处的
    • 因为常量并不添加状态,所以对常量不适用

      第19条:总是初始化变量

  23. 养成在使用内存内存之前先清除的习惯,可以避免为初始化的变量这种错误,在定义变量的时候就应该将其初始化
  24. 使用默认初始化值或? :减少数据流和控制流的混合
  25. 用函数代替复杂的计算流,有时候计算值的最好方式是将计算封装在一个函数中
  26. 例外:硬件或其他进程直接写入的缓冲区数据和volatile型数据不要程序对其进行初始化。

    第20条:避免函数过长,避免嵌套过深

  27. 尽量紧凑:对一个函数只赋予一种职责
  28. 不要自我重复:优先使用命名函数,不要让相似的代码片段反复出现
  29. 优先使用&&:在可以使用&&条件判断的地方要避免使用连续嵌套的if
  30. 不要过分使用try:优先使用析构函数进行自动清除而避免使用try代码块
  31. 优先使用标准算法:算法比循环嵌套要少,通常也更好
  32. 不要根据类型标签进行分支,优先使用多态函数
  33. 例外情况:如果一个函数的功能无法合理地重构为多个独立的子任务,那么它的较长和嵌套较多就是合理的。

    第21条:避免跨编译单元的初始化依赖

  34. 保持初始化顺序:不同编译单元中的名字空间级对象决不应该在初始化上互相依赖,因为其初始化顺序是未定义的。
  35. 在不同编译单元中定义两个名字空间级的对象时,先调用哪一个对象的构造函数是没有定义的。因此,在任何名字空间级对象的初始化代码中,不能假设其他编译单元中定义的任何其他对象都已经初始化了。
  36. 在使用构造函数构造之前,名字空间级对象就已经使用0静态初始化过了。
  37. 如果确实需要可能以来于另一个对象的此种变量,可以考虑使用单体(Singleton)设计模式。

    第22条:尽量减少定义性依赖,避免循环依赖

  38. 不要过分依赖:如果用前向声明(forward declaration)能够实现,就不要包含(#include)定义
  39. 不要循环依赖:循环依赖是指两个模块之间或间接地相互依赖,互相依赖的多个模块并不是真正的独立模块,而是紧紧胶着在一起的更高的模块,一个更大的发布单元。
  40. 除非确实需要类型定义,否则就应该优先使用前向声明,主要在以下两种情况下需要类的完整定义:
    • 需要知道类对象的大小时,例如在栈中创建一个对象
    • 需要命名或者调用类的成员时
  41. 为了避免循环依赖,可以使用依赖倒置原理(Dependency Inversion Principle):不要让高层模块依赖于底层模块,而是应该让两者都依赖于抽象。

    第23条:头文件应该自给自足

  42. 应该确保所编写的每个头文件都能够独自进行编译,为此需要包含其内容所依赖的所有头文件。
  43. 模板是在其定义处编译的,但是非独立的名称或者类型需要等到模板实例化时才编译。 这意味着一个带有std::queue成员的template class Widget即使没有包含deque也不会引发编译期错误。
  44. 只有在使用时才实例化成员函数模板和模板的成员函数

    第24条:总是编写内部#include保护符,绝对不要编写外部#include保护符

  45. 应该用内部包含保护符保护每个头文件,以避免在多次包含时重新定义
  46. 定义包含保护符时,应该遵守以下规则:
    • 保护符使用唯一名称:确保名称至少应该应用程序中是唯一的
    • 不要自作聪明:不要再受保护部分的前后放置代码或者注释,要谨遵上面的标准形式
  47. 例外情况:在非常罕见的情况下,可能需要多次包含一个头文件。
Table of Contents