Google-Code-Style-C++
some personal notes when reading google code style
头文件
self-contained头文件
头文件应该能够自给自足,self-contained,可以作为第一个头文件被引入
如果.h文件声明了一个模板或者内联函数,同时也在该文件加以定义
例外:如果某函数模板为所有相关模板函数显式实例化,或者本身是某个类的一个私有成员,那么就只能定义在实例化该模板的.cc文件
define保护
所有头文件都应该使用#define来防止头文件被多重包含,格式:<PROJECT>_<PATH>_<FILE>_H_
为保证唯一性,头文件的命名应该基于所在项目源代码树的全路径
前置声明
尽可能地避免使用前置声明,使用#include 包含需要的头文件即可
前置声明(forward declaraion)是类、函数和模板的纯粹声明,没有定义
- 优点
- 节省编译时间,#include会使代码因为头文件中无关的改动重新编译多次
- 缺点
- 隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程;
- 可能会被库的后续更改破坏
- 来自命名空间std::的symbol时,行为为定义
- 很难判断什么时候该用前置声明,什么时候用#include
内联函数
只有当函数只有10行甚至更少时才将其定义为内联函数
当函数被声明为内联函数之后,编译器会将其内联展开,而不是按照通常的函数调用机制进行调用
- 经验准则
- 谨慎对待析构函数,析构函数往往比其表面看起来要更长,因为含有隐含的成员和基类析构函数被调用
- 内联包含循环或者switch语句的函数往往得不偿失
- 有些函数即使声明为内联也不一定会被编译器内联,比如虚函数和递归函数
#include的路径和顺序
使用标准头文件包含顺序可增强可读性,避免隐藏依赖
依赖的符号(symbol)被哪些头文件定义,就应该包含这些头文件,前置声明情况除外
避免多重包含
作用域
命名空间
鼓励在.cc文件内使用匿名空间或static声明,禁止使用using指示,精致实用内联名空间
命名空间将全局作用域细分为独立的,具名的作用域,可有效防止全局作用域的命名冲突
- 优点
- 防止冲突(内联名空间主要用来保持跨版本的ABI兼容性
- 缺点
- 命名空间具有迷惑性,使得区分两个相同命名所指代的定义更加困难
- 完整的命名空间导致代码的冗长
- 在头文件中使用匿名空间将违背C++的唯一定义原则
- 结论
- 遵守命名空间命名中的规则
- 在命名空间的最后注释出命名空间的名字
- 不要在命名空间std内声明任何东西,包括标准库的类前置声明
- 不应该使用using指示引入整个命名空间的标识符号
- 不要在头文件中使用命名空间别名,除非显式标记内部命名空间使用
- 禁止使用内联名空间
匿名命名空间和静态变量
在.cc文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为static,但是不要在.h文件中这么做
所有属于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为static拥有内部链接性,意味着此文件中声明的标识都不能在另外一个文件中被访问.即使两个文件生命了完全一样名字的标识符,它们所指向的实体实际上时完全不同的
- 结论
- 推荐鼓励在.cc中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在.h中使用
- 匿名命名空间的声明和具名的格式相同,在最后注释上namespace
非成员函数、静态成员函数和全局函数
使用静态成员函数或命名空间内的非成员函数,尽量不要用裸的全局函数
将一系列函数直接置于命名空间中,不要使用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关
优点: 将非成员函数放在命名空间内可避免污染全局作用域
缺点: 将非成员函数和静态成员函数作为新类的成员或许更有意义,当它们需要访问外部资源或具有重要的依赖关系时更是如此
结论: 为了封装若干不共享任何静态数据的静态成员函数而创建类,不如使用命名空间
局部变量
将函数变量尽可能置于最小作用域内,并在变量声明时进行初始化
C++允许在函数的任何位置声明变量,建议离第一次使用越近越好,有一个例外,如果变量是一个对象,每次进入作用域都要调用构造函数,退出作用域都要调用析构函数,这会导致效率降低
静态和全局变量
禁止定义静态存储周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序时不明确的,这将导致代码的不可移植
原生数据类型(POD:Plain Old Data):即int,char和float,以及POD类型的指针,数组和结构体
类
构造函数的职责
可以在构造函数中进行各种初始化操作,不要在构造函数中调用虚函数,也不要在无法报出错误时进行可能失败的初始化,简单来说,确保在构造函数的行为是确定的
优点
- 不需要考虑类是否被初始化
- 经过构造函数初始化后的对象可以为const类型
缺点
- 构造函数中调用了自身的虚函数,不会重定向到子类的虚函数实现
- 构造函数很难上报错误
- 执行失败会得到一个初始化失败的对象,必须使用bool IsValid()机制检查
- 构造函数的地址无法获取,由构造函数完成的工作是无法以简单的方式交给其他线程
结论
- 构造函数不允许调用虚函数,或者尝试报告一个非致命错误
- 可以考虑使用明确的Init()方法或者使用工厂模式
隐式类型转换
不定义隐式类型转换,对于转换运算符和单参数构造函数,使用explicit关键字
explicit关键字可以用于构造函数或类型转换运算符,以保证只有当目的类型在调用点被显式写明时才能进行类型转换
在类型定义中,类型转换运算符和单参数构造函数都应当用explicit进行标记
例外:拷贝和移动构造函数不应当被标记为explicit,因为它们不执行类型转换
可拷贝类型和可移动类型
可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一 对象的值, 同时不改变源对象的值.
结构体&类
仅当只有数据成员时使用struct, 其它一概使用class
继承
使用组合比使用继承更合理
子类继承基类时会包含所有数据及操作的定义
使用继承的两种场合:
- 子类继承父类的实现代码
- 子类继承父类的方法名称
尽量在”是”的关系下使用继承
多重继承
只在以下情况才推荐使用多重继承:最多只有一个基类是非抽象类
只有当所有父类除第一个外都是纯接口时,才允许使用多重继承
接口
接口是指满足特定条件的类,以Interface为后缀
当一个类满足以下要求时, 称之为纯接口:
- 只有纯虚函数(“=0”)和静态函数(除了下文提到的析构函数)
- 没有非静态数据成员
- 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为protected
- 如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承
运算符重载
除少数特定环境外,不要重载运算符,也不要创建用户定义字面量
存取控制
将所有数据成员声明为private,除非是static const类型成员
声明顺序
将相似的声明放在仪器,将public部分放在最前
函数
参数顺序
输入参数在前,输出参数在后
编写简短函数
倾向于编写简短、凝练的函数,函数超过40行,可以考虑进行分割
引用参数
所有按引用传递的参数必须加上const
函数重载
如果打算重载一个函数,可以改为在函数名里加上参数信息
缺省参数
》 只允许在非虚函数中使用缺省参数,且必须保证缺省参数的值始终一致
函数返回类型后置语法
》 只有在常规写法(返回类型前置)不便于书写或不便于阅读时使用返回类型后置语法
C++现在允许两种不同的函数声明方式
1 | int foo(int x); |
优点:
- 后置返回类型是显式地指定Lambda表达式的返回值的唯一方式
- 有时在已经出现了的函数参数列表之后指定返回类型,能够让书写更简单,也更易读,尤其是在返回类型依赖于模板参数时
例如
1 | template <class T, class U> auto add(T t, U u) -> decltype(t + u); |
类型命名
类型名称的每个单词首字母均大写,不包含下划线
所有类型命名–类,结构体,类型定义,枚举,类型模板参数–均使用相同约定
变量命名
变量(包括函数参数)和数据成员名全部小写,单词之间用下划线连接,类的成员变量以下划线结尾,结构体不用