C++之编程规范

文章介绍了C++编程中的最佳实践,包括头文件的自我包含保护,避免过度使用前置声明,内联函数的使用限制,以及#include路径的顺序。强调了作用域的管理,如局部变量的初始化和静态变量的使用。讨论了命名约定,如变量、常量、函数和类的命名规范。此外,还涵盖了函数参数顺序、构造函数的职责、类型转换、异常处理和RTTI的使用考量。文章还提到了模板编程、Boost库的使用建议以及代码格式的一致性要求。
摘要由CSDN通过智能技术生成


我们承担ROS,FastDDS等通信中间件,C++,cmake等技术的项目开发和专业指导和培训,有10年+相关工作经验,质量有保证,如有需要请私信联系。

谷歌C++风格指南:https://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/

头文件

  • self-contained头文件: 用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾——?
  • define保护:所有头文件都应该使用 #define 来防止头文件被多重包含, 命名格式当是: H .为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

前置声明

尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。

  • 优点:
    • 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
    • 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。——有利有弊,如下面的缺点
  • 缺点:
    • 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
    • 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
    • 很难判断什么时候该用前置声明,什么时候该用 #include
    • 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。

内联函数

  • 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用
  • 内联那些包含循环或 switch 语句的函数常常是得不偿失——?
  • 有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数

#include路径次序

	1. dir2/foo2.h (优先位置, 详情如下)
	2. C 系统文件
	3. C++ 系统文件
	4. 其他库的 .h 文件
	5. 本项目内 .h 文件

作用域

  • 局部变量: 在尽可能小的作用域中声明变量, 离第一次使用越近越好; 应使用初始化的方式替代声明再赋值, 有一个例外, 在循环作用域中,如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.
  • 匿名命名空间和静态变量: 所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为 static 拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。结论是: 推荐、鼓励在 .cc 中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在 .h 中使用。
  • 非成员函数,静态成员函数和全局函数:
    • 使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数.
    • 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果(?),类的静态方法应当和类的实例或静态数据紧密相关.
    • 所以如果必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名 命名空间 或 static 链接关键字 (如 static int Foo() {…}) 限定其作用域.
// 应为:
namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
}  // namespace foo_bar
}  // namespace myproject
// 而非:
namespace myproject {
class FooBar {
public:
  static void Function1();
  static void Function2();};
}  // namespace myproject

静态和全局变量

  • 静态生存 周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体
  • 静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数(如 getenv() 或 getpid() )不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。( 同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为) 同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从 main() 返回还是对 exit() 的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。 综上所述,我们只允许 POD 类型的静态变量,即完全禁用 vector (使用 C 数组替代) 和 string (使用 const char [])。 如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

函数

  • 参数顺序:输入参数在先,后跟输出参数。 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则
  • 编写简短, 凝练的函数, 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割
  • 所有按引用传递的参数必须加上 const。事实上这在 Google Code 是一个硬性约定: 输入参数是值参或 const 引用, 输出参数为指针. 输入参数可以是 const 指针, 但决不能是非 const 的引用参数, 除非特殊要求, 比如 swap().——这是为什么?
  • 缺省参数: 只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致。 对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作.

  • 构造函数的职责:不要在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化.
    • 缺点:
      • 在没有使程序崩溃 (因为并不是一个始终合适的方法) 或者使用异常 (因为已经被 禁用 了) 等方法的条件下, 构造函数很难上报错误
      • 如果执行失败, 会得到一个初始化失败的对象, 这个对象有可能进入不正常的状态, 必须使用 bool IsValid() 或类似这样的机制才能检查出来, 然而这是一个十分容易被疏忽的方法.
      • 构造函数的地址是无法被取得的, 因此, 举例来说, 由构造函数完成的工作是无法以简单的方式交给其他线程的.
    • 结论: 构造函数不允许调用虚函数. 如果代码允许, 直接终止程序是一个合适的处理错误的方式. 否则, 考虑用 Init() 方法或工厂函数.
  • 隐式类型转换: 不要定义隐式类型转换. 对于转换运算符和单参数构造函数, 请使用 explicit 关键字.
    • 用法: explicit 关键字可以用于构造函数或 (在 C++11 引入) 类型转换运算符, 以保证只有当目的类型在调用点被显式写明时才能进行类型转换
    • 结论: 在类型定义中, 类型转换运算符和单参数构造函数都应当用 explicit 进行标记. 一个例外是, 拷贝和移动构造函数不应当被标记为 explicit, 因为它们并不执行类型转换。 不能以一个参数进行调用的构造函数不应当加上 explicit. 接受一个 std::initializer_list 作为参数的构造函数也应当省略 explicit, 以便支持拷贝初始化 (例如 MyType m = {1, 2}😉 .
  • 可拷贝类型和可移动类型:
    • 如果类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用.
  • struct vs class: 仅当只有数据成员时使用 struct, 其它一概使用 class. struct 用来定义包含数据的被动式对象, 也可以包含相关的常量
  • 继承: 使用组合常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.
  • 多重继承:真正用到多重继承的情况少之又少;只有在以下情况下才允许使用多重继承:最多只有一个基类是非抽象类;其他基类都是以 Interface 为后缀的纯接口类;
    • 结论:只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以 Interface 为后缀.
  • 接口:
  • 运算符重载: 除少数特定环境外, 不要重载运算符. 也不要创建用户定义字面量。 不要重载 &&, ||, , 或一元运算符 &. 不要重载 operator"", 也就是说, 不要引入用户定义字面量.
    • 存取控制: 将 所有 数据成员声明为 private, 除非是 static const 类型成员
    • 声明顺序: 将相似的声明放在一起, 将 public 部分放在最前。说明:
      • 类定义一般应以 public: 开始, 后跟 protected:, 最后是 private:. 省略空部分.
      • 在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它函数, 数据成员.

其他C++特性

  • 右值引用:只在定义移动构造函数与移动赋值操作时使用右值引用. 不要使用 std::forward.
    • 缺点: 右值引用是一个相对比较新的特性 (由 C++11 引入), 它尚未被广泛理解. 类似引用崩溃, 移动构造函数的自动推导这样的规则都是很复杂的.
  • 尽量不使用函数重载
  • 异常:不使用异常;对于到底需不需要使用异常,参考: https://www.zhihu.com/question/22889420
    • 优点:
      • 异常是处理构造函数失败的唯一途径
      • 有些第三方库依赖异常,禁用异常就不好用了
      • 在测试框架很有用
    • 缺点(还需整理)
      • 在现有函数中添加 throw 语句时,必须检查所有调用点。要么让所有调用点统统具备最低限度的异常安全保证,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。举例,f() 调用 g(), g() 又调用 h(), 且 h 抛出的异常被 f 捕获。当心 g, 否则会没妥善清理好。
      • 还有更常见的,异常会彻底扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。您或许会加一大堆何时何处处理异常的规定来降低风险,然而开发者的记忆负担更重了。
      • 异常安全需要RAII和不同的编码实践. 要轻松编写出正确的异常安全代码需要大量的支持机制. 更进一步地说, 为了避免读者理解整个调用表, 异常安全必须隔绝从持续状态写到 “提交” 状态的逻辑. 这一点有利有弊 (因为你也许不得不为了隔离提交而混淆代码). 如果允许使用异常, 我们就不得不时刻关注这样的弊端, 即使有时它们并不值得.
      • 启用异常会增加二进制文件数据,延长编译时间(或许影响小),还可能加大地址空间的压力。
      • 滥用异常会变相鼓励开发者去捕捉不合时宜,或本来就已经没法恢复的「伪异常」。比如,用户的输入不符合格式要求时,也用不着抛异常。如此之类的伪异常列都列不完。
  • 运行时类型识别: RTTI 允许程序员在运行时识别 C++ 类对象的类型. 它通过使用 typeid 或者 dynamic_cast 完成.
    • 优点: RTTI 在某些单元测试中非常有用. 比如进行工厂类测试时, 用来验证一个新建对象是否为期望的动态类型. RTTI 对于管理对象和派生对象的关系也很有用.
    • 缺点:
      • 在运行时判断类型通常意味着设计问题. 如果你需要在运行期间确定一个对象的类型, 这通常说明你需要考虑重新设计你的类.
      • 随意地使用 RTTI 会使你的代码难以维护. 它使得基于类型的判断树或者 switch 语句散布在代码各处. 如果以后要进行修改, 你就必须检查它们.
    • 结论:RTTI 有合理的用途但是容易被滥用, 因此在使用时请务必注意. 在单元测试中可以使用 RTTI, 但是在其他代码中请尽量避免. 尤其是在新代码中, 使用 RTTI 前务必三思. 如果你的代码需要根据不同的对象类型执行不同的行为的话, 请考虑用以下的两种替代方案之一查询类型:
      • *虚函数可以根据子类类型的不同而执行不同代码. 这是把工作交给了对象本身去处理.
      • 如果这一工作需要在对象之外完成, 可以考虑使用双重分发的方案, 例如使用访问者设计模式. 这就能够在对象之外进行类型判断.
      • 如果程序能够保证给定的基类实例实际上都是某个派生类的实例, 那么就可以自由使用 dynamic_cast. 在这种情况下, 使用 dynamic_cast 也是一种替代方案.
      • 注意:基于类型的判断树是一个很强的暗示, 它说明你的代码已经偏离正轨了. 不要像下面这样:
if (typeid(*data) == typeid(D1)) {
...} else if (typeid(*data) == typeid(D2)) {
...} else if (typeid(*data) == typeid(D3)) {...

一旦在类层级中加入新的子类, 像这样的代码往往会崩溃. 而且, 一旦某个子类的属性改变了, 你很难找到并修改所有受影响的代码块。不要去手工实现一个类似 RTTI 的方案. 反对 RTTI 的理由同样适用于这些方案, 比如带类型标签的类继承体系. 而且, 这些方案会掩盖你的真实意图.

  • 类型转换: 使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等转换方式
    • 优点: C 语言的类型转换问题在于模棱两可的操作; 有时是在做强制转换 (如 (int)3.5), 有时是在做类型转(如 (int)“hello”). 另外, C++ 的类型转换在查找时更醒目——?难道第一个不是强制类型转换
    • 结论:
      • 用 static_cast 替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时.
      • 用 const_cast 去掉 const 限定符.
      • 用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换. 仅在你对所做一切了然于心时使用.
  • 流: 只在记录日志时使用流. 流用来替代 printf() 和 scanf().——?不只是这两个吧,文件流,string流…
    • 优点: 有了流, 在打印时不需要关心对象的类型. 不用担心格式化字符串与参数列表不匹配 (虽然在 gcc 中使用 printf 也不存在这个问题). 流的构造和析构函数会自动打开和关闭对应的文件.
    • 缺点:流使得 pread() 等功能函数很难执行. 如果不使用 printf 风格的格式化字符串, 某些格式化操作 (尤其是常用的格式字符串 %.*s) 用流处理性能是很低的. 流不支持字符串操作符重新排序 (%1s), 而这一点对于软件国际化很有用.——?
    • 结论: 不要使用流, 除非是日志接口需要. 使用 printf + read/write之类的代替.——主要还是从可读性,易出错性等方面来说的
  • 前置自增/自减: 对简单数值 (非对象), 两种都无所谓. 对迭代器和模板类型, 使用前置自增 (自减).
  • const:
    • 缺点: const 是入侵性的: 如果你向一个函数传入 const 变量, 函数原型声明中也必须对应 const 参数 (否则变量需要 const_cast 类型转换), 在调用库函数时显得尤其麻烦.
    • 结论: const 变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障; 便于尽早发现错误. 因此强烈建议在任何可能的情况下使用 const,此外有时改用 C++11 推出的 constexpr 更好; mutable 可以使用, 但是在多线程中是不安全的, 使用时首先要考虑线程安全.
  • constexpr: 在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。 变量可以被声明成 constexpr 以表示它是真正意义上的常量,即在编译时和运行时都不变。函数或构造函数也可以被声明成 constexpr, 以用来定义 constexpr 变量。
    • 结论: 靠 constexpr 特性,方才实现了 C++ 在接口上打造真正常量机制的可能。好好用 constexpr 来定义真常量以及支持常量的函数。避免复杂的函数定义,以使其能够与constexpr一起使用。 千万别痴心妄想地想靠 constexpr 来强制代码「内联」。
  • 整型: <stdint.h> 定义了 int16_t, uint32_t, int64_t 等整型, 在需要确保整型大小时可以使用它们代替 short, unsigned long long 等. 在 C 整型中, 只使用 int. 在合适的情况下, 推荐使用标准类型如 size_t 和 ptrdiff_t.对于大整数, 使用 int64_t.不要使用 uint32_t 等无符号整型, 除非你是在表示一个位组而不是一个数值, 或是你需要定义二进制补码溢出. 尤其是不要为了指出数值永不会为负, 而使用无符号类型. 相反, 你应该使用断言来保护数据.
  • 64位下的可移植性
  • 预处理宏: 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.如果你要宏, 尽可能遵守:
    • 不要在 .h 文件中定义宏.
    • 在马上要使用时才进行 #define, 使用后要立即 #undef.
    • 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
    • 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.
    • 不要用 ## 处理函数,类和变量的名字。
  • nullptr和NULL: 整数用 0, 实数用 0.0, 指针用 nullptr 或 NULL, 字符 (串) 用 ‘\0’.
  • sizeof: 尽可能用 sizeof(varname) 代替 sizeof(type). 使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新——?这个解释怎么理解?
  • auto: 可以用 auto 来复制初始化或绑定引用——? auto 在涉及迭代器的循环语句里挺常用
    • 只在局部变量里用,别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化 auto 变量。
    • auto 还可以和 C++11 特性「尾置返回类型(trailing return type)」一起用,不过后者只能用在 lambda 表达式里。
auto x(3);  // 圆括号。    它们不是同一回事——x 是 int
auto y{3};  // 大括号。    y 则是 std::initializer_list<int>.
  • lambda表达式:
    • 优点:
      • 传函数对象给 STL 算法,Lambdas 最简易,可读性也好。
      • Lambdas, std::functions 和 std::bind 可以搭配成通用回调机制(general purpose callback mechanism);写接收有界函数为参数的函数也很容易了。——?
    • 缺点:
      • Lambdas 的变量捕获略旁门左道,可能会造成悬空指针。
      • Lambdas 可能会失控;层层嵌套的匿名函数难以阅读。
    • 结论:
      • 按 format 小用 lambda 表达式怡情。
      • 禁用默认捕获,捕获都要显式写出来。比起 [=](int x) {return x + n;}, 应写成 [n](int x) {return x + n;} 才对,这样读者也好一眼看出 n 是被捕获的值。
      • 匿名函数始终要简短,如果函数体超过了五行,那么还不如起名(即把 lambda 表达式赋值给对象),或改用函数。
      • 如果可读性更好,就显式写出 lambd 的尾置返回类型,就像auto.
  • 模板编程: 除了优点外,大量的使用模板编程接口会让重构工具(Visual Assist X, Refactor for C++等等)更难发挥用途. 首先模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用, 其次有些重构工具只对已经做过模板类型替换的代码的AST 有用. 因此重构工具对这些模板实现的原始代码并不有效, 很难找出哪些需要重构.
    • 结论:模板编程最好只用在少量的基础组件, 基础数据结构上, 因为模板带来的额外的维护成本会被大量的使用给分担掉
  • boost库: 只使用 Boost 中被认可的库:
    在这里插入图片描述

命名约定

文件命名

  • 全部小写, 可以包含下划线 () 或连字符 (-), 依照项目的约定. 如果没有约定, 那么 “” 更好.
  • C++ 文件要以 .cc 结尾, 头文件以 .h 结尾. 专门插入文本的文件则以 .inc 结尾, 参见 头文件自足
  • 定义类时文件名一般成对出现, 如 foo_bar.h 和 foo_bar.cc, 对应于类 FooBar.
  • 内联函数必须放在 .h 文件中. 如果内联函数比较短, 就直接放在 .h 中.——为什么是必须?

类型命名

所有类型命名类, 结构体, 类型定义 (typedef), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线

变量命名

  • 普通变量: 变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接
  • 类数据成员: 不管是静态的还是非静态的, 类数据成员都可以和普通变量一样, 但要接下划线.
  • 结构体变量:不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类接下划线

常量命名

声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合,如const int kDaysInAWeek = 7; 所有具有静态存储类型的变量 (例如静态变量或全局变量) 都应当以此方式命名

函数命名

  • 常规函数使用大小写混合(即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线. 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 StartRpc() 而非 StartRPC()).
  • 取值和设值函数则要求与变量名匹配, 一般来说它们的名称与实际的成员变量对应 , 但并不强制要求

命名空间命名

命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称。命名空间中的代码, 应当存放于和命名空间的名字匹配的文件夹或其子文件夹中. 注意 不使用缩写作为名称 的规则同样适用于命名空间

枚举命名

枚举的命名应当和 常量 或 宏 一致: kEnumName 或是 ENUM_NAME. 单独的枚举值应该优先采用 常量 的命名方式

格式

  • 行长度: 每一行代码字符数不超过 80.
  • 非ASCII字符: 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.——这个怎么区分?
  • 空格缩进
  • 函数声明与定义

cpplint使用总结

TODO

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值