为什么要做这个摘录?因为我在学习 C++ 代码的时候发现它的功能优点过于强大,并且代码书写上面没有很强的限制,C++ 经过几十年的迭代其实背负了很多旧时代的气息,这也导致了不同时代不同背景的人写 C++ 代码有不同的习惯,所以想尽量借鉴一下优秀的团队是怎么写 C++ 代码的,所以就有了这一篇摘录。
内联函数
只有当函数只有 10 行甚至更少时才将其定义为内联函数。
滥用内联将导致程序变得更慢。 内联可能使目标代码量或增或减, 这取决于内联函数的大小。 内联非常短
小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小。
另外不要内联那些包含循环或 switch 语句的函数、虚函数和递归函数,这些函数通常不能被很好的内联。
尽量不用全裸全局函数
将非成员函数放在名字空间内可避免污染全局作用域。这点其实蛮重要的,这样可以减少不必要的耦合和链接时依赖。
注意声明变量的位置
一般来说提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好。 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值。
但是也有些例外:
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f;
// 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数。在循环作用域外面声明这类变量要高效的多:
// 构造函数和析构函数只调用 1 次
Foo f;
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
禁止使用 class 类型的静态或全局变量
类类型的全局变量的构造函数、析构函数和初始化的顺序在 C++ 中是不确定的,甚至随着构建变化而变化,导致难以发现的 bug, 并且析构顺序也是不定的。
类的初始化
如果类中定义了成员变量, 则必须在类中为每个类提供初始化函数或定义一个构造函数。 若未声明构造函数, 则
编译器会生成一个默认的构造函数, 这有可能导致某些成员未被初始化或被初始化为不恰当的值。
这一点不像 java 或 go,它们都有个默认初始化值,但是 C++ 没有,所以如果你的类中有成员变量没有在类里面进行初始化, 而且没有提供其它构造函数, 你必须定义一个 (不带参数的) 默认构造函数。 把对象的内部状态初始化成一致/ 有效的值无疑是更合理的方式。
可拷贝类型和可移动类型
如果你的类型需要, 就让它们支持拷贝/ 移动。 否则, 就把隐式产生的拷贝和移动函数禁用。
拷贝/ 移动构造函数一般来说有它们的特定用途,比如可以让代码更加简洁,并且性能会更好,但是它是一种隐式的操作,经常会让人迷惑和忽略。如果你的类不需要拷贝/ 移动操作, 请显式地通过 = delete 或其他手段禁用。
重载运算符
除少数特定环境外,不要重载运算符。
使代码看上去更加直观, 类表现的和内建类型 (如 int) 行为一样可以使用 + 和 / 等运算符。 但是这种隐式的操作容易混淆视听,让你觉得耗时的操作和操作内建类型一样轻巧。并且更难定位重载运算符的调用点, 查找 Equals() 显然比对应的 == 调用点要容易的多。
所以一般不要重载运算符。 尤其是赋值操作 (operator=) 比较诡异, 应避免重载。 如果需要的话, 可以定义类似Equals(), CopyFrom() 等函数。
不使用C++异常
以前写 java 项目的时候,经常使用异常来处理错误,然后在外层包一个很大的 try catch 进行捕获,这也就意味着想要使用异常必须检查所有调用点,否则就眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。
并且异常容易扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。并且滥用异常会变相鼓励开发者去捕捉不合时宜,或本来就已经没法恢复的「伪异常」。
取而代之可以使用错误代码, 断言等操作使我们的程序保持正常运行。
尽量使用 C++ 的类型转换
很多同学都喜欢在 C++ 代码中使用 C 风格的类型转换 int y = (int)x 或 int y = int(x) 。但是 C 语言的类型转换问题在于模棱两可的操作; 有时是在做强制转换 (如 (int)3.5), 有时是在做类型转换(如 (int)"hello")。
所以我们应该:
- 用 static_cast 替代 C 风格的值转换,或某个类指针需要明确的向上转换为父类指针时。
- 用 const_cast 去掉 const 限定符。
- 用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换。 仅在你对所做一切了
然于心时使用。
应使用前缀自增(++i),自减运算符
不考虑返回值的话, 前置自增 (++i) 通常要比后置自增 (i++) 效率更高。 因为后置自增 (或自减) 需要对表 达式的值 i 进行一次拷贝。 如果 i 是迭代器或其他非数值类型, 拷贝的代价是比较大的,当然这种说法只是对自定义类型上,如果是内置类型而言,大部分编译器会做优化,因此效率没什么区别。
不用使用无符号整型
不要使用 uint32_t 等无符号整型, 除非你是在表示一个位组而不是一个数值, 或是你需要定义二进制补
码溢出。 尤其是不要为了指出数值永不会为负, 而使用无符号类型。 相反, 你应该使用断言来保护数据。
小心整型类型转换和整型提升(acgtyrant 注:integer promotions, 比如 int 与 unsigned int 运算时,
前者被提升为 unsigned int 而有可能溢出),总有意想不到的后果。
关于无符号整数:
有些人, 包括一些教科书作者, 推荐使用无符号类型表示非负数。 这种做法试图达到自我文档化。 但是, 在
C 语言中, 这一优点被由其导致的 bug 所淹没。 看看下面的例子:
for (unsigned int i = foo.Length()-1; i >= 0; --i) ... // 关于这点我还弄出过bug
上述循环永远不会退出! 有时 gcc 会发现该 bug 并报警, 但大部分情况下都不会。 类似的 bug 还会出现在
比较有符合变量和无符号变量时。
尽量以const,enum,inline替换#define
目前很多 C++ 代码还是保留着 C 时代时使用宏的习惯。C++ 中, 宏不像在 C 中那么必不可少。 以往用宏展开性能关键的代码, 现在可以用内联函数替代,用宏表示常量可被 const 变量代替。
举一个常见的例子,比如我们使用了以下这个宏:
#define PI 3.14
如上述的 PI
宏定义,在程序编译时,编译器在预处理阶段时,会先将源码中所有 PI
宏定义替换成 3.14。程序编译在预处理阶段后,才进行真正的编译阶段。在有的编译器,运用了此 PI
常量,如果遇到了编译错误,那么这个错误信息也许会提到 3.14 而不是 PI
,这是会让人困惑的,特别是在项目大的情况下。
总之使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之。
对于命名规范和代码风格
不同团队有不同的规范和代码风格,应该尽量参考原来项目的来做,而不是自己新加一种风格,这样在读代码的时候会感觉非常奇怪。为了与代码原有风格保持一致,如果不放心应该与代码原作者或现在的负责人员商讨。
Reference
http://staff.ustc.edu.cn/~tongwh/CG_2019/materials/Google%20C++%20Style%20Guide.pdf