Google C++ 代码规范
Google提出的一套代码规范,能提高代码的可读性,减少出错。建议配合Effective C++阅读
头文件
所有头文件都应该自给自足(self-contained)
self-contained : compile on their own
确保你的header files包含了你需要的所有东西,而不是假设你#include进来的某个(些)headers帮你包含了你需要的东西。
A.h包含了B.h和C.h,而B.h也包含了C.h,那么在A.h中使用C.h时,会难以判断到底该用哪一个
#define 保护
每个头文件都要使用#pragma once
保护
前置声明
我个人理解的前置声明,是指在一个.h
文件中开头声明另一个.h
文件中的类,以此来实现类似#include
某个文件的某一部分
与之对应的是直接使用#include
引入文件,但这样实际会增加编译工作量,降低编译速度,于是我们非必要不#include
Google建议要避免使用前置声明,因为使用前置声明会影响类的修改,也容易产生错误
但使用前置声明可以提高编译速度,因此需要按情况选择
优点
- 节省编译时间,避免很多不必要的(因为头文件改动而导致的)重新编译
缺点
- 隐藏了依赖关系
- 头文件改动时会破坏后续,影响开发者改动API(比如当你要添加一个形参时)
- 但这个是不是可以通过宏的方法避免?
#define Func Func
- 但这个是不是可以通过宏的方法避免?
两个类互相引用
前置声明(Forward Declarations)基本仅用于两个类互相引用
// A.h |
// B.h |
失去依赖关系
前置声明最大的问题是失去依赖关系
// B.h |
|
内联函数
除非是极其高频调用,确定是性能瓶颈的地方,不然不用内联函数
不要内敛超过十行的函数
析构函数远比表面要长,因为他隐式调用着成员和基类的析构函数
#include路径
按照源码目录树结构排列,避免使用UNIX的快捷目录,比如.
(当前目录)和..
(上级目录)
#include顺序
- C头文件
- C++头文件
- 第三方库头文件
- 本项目头文件
作用域
命名空间将全局作用域细分为独立具体的作用域,可以有效防止全局命名冲突
|
请在命名空间最后注释出命名空间的名字
不要污染命名空间
- 不要在std命名空间中声明任何东西
- 不要使用
using namespace xxx
- 不要在头文件中使用命名空间别名,如
namespace bbb = ::Foo::Bar::Baz
静态变量
不要在.h文件中声明静态变量,至少不能用全裸的静态变量
不要定义静态存储周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中静态变量的构造和析构顺序不确定,会导致bug(除非你使用constexpr)
原生数据类型(POD,Olain Old Data),如int、char、float,int*、int[]、结构体
在同一编译单元,静态初始化会优先于动态初始化,并按照声明顺序初始化,以逆序销毁
但是不同编译单元,初始化、销毁顺序是未定义行为
类
构造函数不要调用虚函数
在类被完全初始化前,这个类其实是基类+其他部分,此时调用虚函数不一定会指向子类的重载,可能会报错、出错
优点
- 不需要考虑类是否被完全初始化
- 初始化的对象可以为const类型
如果你想进行有意义的non-trivial初始化,可以使用明确的Init()方法,或者工厂模式
与之对应,析构函数要设为虚函数
不要隐式类型转换
不要使用隐式类型转换
- 隐式转换会降低代码可读性,由于是存在函数重载时,让人难以判断到底在用哪一个函数
- 隐式转化可能会导致类型不匹配的错误
使用explict关键词(常用于单参数构造函数和类型转化操作符),可以使当前类型只能被显示类型转换,如果使用隐式转化,会报错
class Foo |
拷贝和移动
如果你的类型需要拷贝和支持,就请实现它,否则禁用它(=delete
)
std::unique_ptr<int>
可移动,但是不能复制
- 可拷贝类型,可以使用相同类型的另一对象进行初始化,当前对象会得到相同的值,同时不改变源对象。
- 用户通过定义拷贝构造函数、拷贝赋值操作符实现
- 可移动对象,可以使用相同类型的临时对象进行初始化,当前对象会得到相同的值
- 用户通过定义移动构造函数、移动赋值操作符实现
不要为任何有可能有子类的类型提供拷贝和移动,可能会导致对象割裂
除了数据成员,其他一律用class
C++的class和struct很像,大部分功能相同,不过
- struct可以拥有成员变量,但不能拥有成员函数(你可以写非成员函数+引用)
- struct成员为public,而class可以拥有private成员
- struct的继承方式为public,比class少(尽管全世界C++项目99%的继承都是public继承)
struct与pairs、tuples
struct的字段名更具可读性,当数据有意义时,尽量使用struct
不过pairs和tuples更适用于泛型编程
组合与继承
组合很好用,能用接口用接口,如果非要用继承,请public继承
对于虚函数进行重载时,用override、final标记,尽管这个关键字没有什么作用,但能提高代码可读性
全世界C++项目99%的继承都是public继承
public、protected、private继承你可以理解为设限
如果是public继承,那么子类访问父类成员的存取类型都不会超高public(废话),父类的public、protected、private成员,对于子类为public、protected、private
如果是protected继承,那么子类访问父类成员的存取类型都不会超高protected,父类的public、protected、private成员,对于子类为protected、protected、private
请不要使用多重继承,不过你能继承多个接口
关于接口
- 接口是用interface标记的类,只有纯虚数和静态函数,没有非静态成员
- 接口不能被直接实例化,也不需要定义构造函数
- 请为接口实现虚析构函数
- 请不要为其添加函数实现或非静态成员数据
操作符重载
尽量别重载
访问控制
类的数据成员应该为private,除非是一个常量
数据成员设为private,然后编写public的访问函数,不过感觉不如C#的属性优雅
声明顺序
随便找个
.h
文件,看看类的声明是怎么写的
- 相似的声明放在一起,并按以下顺序
- 类型及类型别名(
typedef, using, enum
,嵌套的结构体和类) - 静态常量
- 工厂函数
- 构造函数和赋值运算符
- 析构函数
- 其他函数
- 数据成员
- 类型及类型别名(
- 先写public,再写protected、private
不要将大段的函数定义写在类定义中,建议.h
声明,.cpp
定义
函数
参数
见名知意,知行合一
看到函数名就能知道这是干什么的,比如Get、Set,如果一个函数是Get,那就做Get的事
- 优先使用返回值而非输出参数,这能提高可读性和性能
- 不要返回空指针(除非你是这样设计的)
- 输入参数、不需要改变的参数可以用const引用
- 输出参数、可以被修改的参数可以用指针
- 参数需要排序,输入先于输出,无默认值先于有默认值
void foo(const std::string& in_str, std::string* out_str); |
简短的函数
个人感觉完全没必要,尤其是对于C#、Java,40行好干什么
简短的函数能提高代码可读性,提高调试效率,在函数式编程很常用
函数重载
函数重载能够使同一作用域中,有一组相同函数名、不同参数列表的函数,常用于命名一组功能相似的函数
不过如果函数仅靠参数类型进行重载,就会涉及匹配、派生类之类的问题,让人感受困惑
如果要重载Append(),其实可以在函数名上添加类型信息,比如AppendInt()、AppendString()
缺省参数
写缺省不如写重载
缺省本质上就是一种函数重载,所有不适合使用重载的地方,都不适合使用缺省
由于缺省参数会干扰函数指针,因此一定不要在虚函数中使用缺省参数
函数返回值的后置写法
后置写法本身没问题,swift、js等语言都是这样写的,但是对于C++来说是一种“很新的东西”,容易让人感到困惑
C++11后,C++的函数可以使用后置返回类型,不过除了lambda表达式,一般不这样写
// 前置写法 |
在泛型编程中,当返回类型依赖于模版参数时,也可以使用后置写法,能提高可读性
// 前置写法 |
所有权和智能指针
关于所有权和智能指针,可以参考垃圾回收中引用计数法
C++标准鼓励我们使用智能指针管理资源
对于一个动态的对象,我们更倾向于让其拥有单一、固定所有权(ownership),并使用指南指针做所有权转换
std::unique_ptr
,独占资源- 当指针离开作用域,资源就会被销毁
- 无法复制(copy),但能转移(move)所有权
std::shared_ptr
,共享资源- 当资源失去所有引用时,资源被销毁
- 可以复制,共享所有权(无需转移)
好处
- 有的对象甚至没法复制,只能转移
- 转移通常比复制更高效,尤其是一些const对象,转移比深拷贝高效得多
- 使用智能指针能提高可读性,也减少了内存泄漏
其他
右值引用
关于左右值,建议阅读C++11
只在定义移动构造函数和移动赋值操作时使用右值引用
推荐使用std::move
,不要使用std::forward
(除非你在模版编程)
// copy |
forward的作用是什么?
如果不使用forward,我们需要同时定义copy和move函数,在处理左值时调用copy,处理右值调用move,这增加了代码量,如果使用forward,我们只需要写一份
但forward让一个函数能干两种事,这不符合“知行合一”,为了代码可读性,我们通常不会使用。如果我们明确这个函数要move,那就用move
友元
友元:在定义一个类时,可以将一些(定义在外部的)函数声明为友元,这些友元函数可以访问该类的private、protected成员
- 友元扩大了类的封装,在OOP中很忌讳,但只要合理,还是可以用的
- 原本是private的成员,对于友元类、函数,都是public的
- 友元类似一种许可,当一个类设置友元后,相当于给友元开了管理员权限,能随意访问原本受保护的成员
- 如果你要用,请写在同一个文件中
class Child{ |
异常
谷歌不使用C++异常,让异常Let it crash。不过异常在C#、Java、Swift中很常用,
抛异常可能会导致一些未定义行为,比如不要在析构函数中使用异常
不使用异常,如果出错常用方法为:
- 直接
abort()
- 吞异常,Let it crash
RTTI
不使用运行时类型识别,不使用typeid
和dynamic_cast
运行时类型识别会使代码难以维护,如果你需要用RTTI(除了单元测试),说明你的类设计的不好
类型转化
使用C++风格的类型转化(而非C风格的)
double double_value; |
- 使用
static_cast
做值转换、子类指针转父类指针 - 使用
const_cast
去掉const限定符 - 使用
reinterpret_cast
做指针和整型、指针和指针间的转换(仅在你会用时使用) - 不使用
dynamic_cast
严格别名
C++的
reinterpret_cast
不会编译为任何CPU指令,会编译为纯汇编,于是你可以像汇编一样操作指向内存的指针地址,破坏了C++对汇编对抽象
严格别名(strict aliasing)规定:只有同一种类型的指针,才可以出现别名,总之没事别做不相干类型的指针转化
float* f; |
当你通过指针,将一个struct塞到一个buffer中,或者将一个buffer塞到struct中,在这个过程中,指针的类型发生转换,从buffer转化为struct
typedef struct Msg { |
上面这个过程干了什么?你知道这个buffer的内存地址、长度,你想直接用这些信息访问内存,这是对的,但是某些情况下这就是未定义行为
流
谷歌认为除了日志接口需要,不要使用流
说实话我感觉流挺方便的,只要能保持输出的一致性就行,而且C++的Stream类型安全,用起来十分方便,尤其是输出字符串和对象时
自增与自减
谷歌建议一律使用前置自增自减,尤其是迭代器
一般而言,使用前置自增++i
能提高性能,后置会生成临时对象和拷贝复制
不过,如果自增自减后变量并没有被用到,仅仅是用于记录迭代次数,编译器会对后置自增自减进行优化,性能一样,而且后置更像自然语言,可读性更强
const
在任何能使用const的地方使用const或constexpr
整型
C++内置的整型只,只使用int
如果需要明确长度,那就用<cstdint
中的int16_t
、int64_t
如果你不确定用何种大小的int,那么用最大的
可移植性
代码应该对32位和64位系统友好,在处理打印、比较、结构体对齐时要注意
不是所有人都在用64位系统(不过iOS这种封闭平台确实做到了完全禁用32位)
预处理宏
尽管在图形Shader中,用宏进行条件变异很常用,但这其实是因为GPU对分支的处理不好,因此我们通过编译多份代码来避免运行时使用分支。不过这对CPU代码纯属是提高包体,降低可读性,增加调试难度,不推荐使用
-
宏具有全局作用域,使你看到的和编译器看到的内容不同,尽量用内联、const进行替换
-
不要使用宏做条件编译
-
不要在
.h
文件中定义宏 -
使用完
#define
后要立刻#undef
-
不要使用展开后让C++构造不稳定的宏
|
- 不要在宏后面写单行注释
- 不要用
##
处理函数、类、变量的名字,可读性很差
// 用##将两个宏拼起来,很trick的写法 |
nullptr
空指针用nullptr,数值用0(或者0.0),std::string
用\0
绝对不要用NULL
sizeof
sizeof用于获取类型的大小,不过不建议对类型使用,而是对varname使用
MyStruct data; |
类型推断
函数模版参数类型推断
template<typename T> |
类模版类型推断
Class Template Argument Deduction(CTAD)
std::array a = {1, 2, 3}; // a is a std::array<int, 3> |
auto变量类型推断
若一个变量被声明为auto,那么它的类型会自动匹配成初始化表达式的类型
auto a = 42; // a is an int |
有的变量类型巨长(尤其是使用模版和命名空间时),而且一次初始化要写两遍,使用auto能提高可读性
- 仅在局部变量中使用auto,比如for循环
- 仅在一眼能看出类型的地方使用auto
- 不要列表初始化auto变量
for(auto& i: list){...} |
函数返回值类型推断
不要用于函数返回值,除非你是lambda使用后置返回值
auto f() -> int { return 1; } |
指定的初始化
C++20才加入的新东西,感觉大家的项目基本都没法用吧
可以方便的生成高可读性高聚合的初始化表达式,尤其对一些字段顺序不明显的结构
struct Point { |
Lambda表达式
- 将所有的捕获显示写出来
- 只有lambda声明周期很短时,才使用
[&]
- 使用
[=]
显式捕获
[=](int x) { return x + n; } // 默认捕获方式,不好 |
泛型编程
避免复杂的泛型编程,这东西可读性相当差,调试难度相当高
别名
别名(Aliases)能让复杂类型的名字变简单,尤其是那些带有命名空间的类型
typedef Foo Bar; // 传统方法 |
- 不要在公共区域使用别名,除非你写对其写详细注释
- 不要在公共区域引入命名空间别名
命名规范
命名规范因项目而异,只要保持一致性即可,谷歌的命名规范我个人不习惯,可能我C#、Swift写的比较多,感觉好奇怪
通用规范
命名别嫌长,要见名知意
你看看人家Java的方法名长度
不要用汉语拼音
你要记住,英语是表音文字,因此可以靠读音/26个字母排列明确意义,而汉字是象形文字,形状才能明确意义,只靠汉语拼音无法明确区分同音字词。
而且汉语拼音是新中国扫盲、普及普通话的工具,不仅外国人看不懂,甚至一些说方言的人也看不懂
慎用缩写
日本人用假名翻译“龙”,还省去了几个词,结果/ˈdræɡən/硬是读成了“多拉贡”
注释
见名知意,自文档的代码确实好,但有时你还是需要写注释,以提高可读性
- 每个文件开头写入版权公告(不要轻易删除原作者的信息)
- 代码段行前注释,描述类行后注释,行后注释要对齐
- 假设读代码的人水平比你高,有些过于明显的API解释,不要写(不会有人给
i++
写注释吧) - 标点、换行(对于python,yml很重要)建议依赖IDE,统一即可
- 写TODO(比如Xcode就支持
TODO:
和MARK:
,很多IDE都能检查) - 如果一个接口被弃用,可以写弃用注释
DEPRECATED
格式
个人建议格式依赖IDE,如果你有自己的坚持,那么先配置IDE
- 一行不能太长,不然别人要缩放屏幕或者拖动水平滚动条,
#include
语句除外 - 使用UTF-8编码(GB-2312的中文注释用UTF-8打开就是乱码)
- 只使用空格而非制表符,IDE都支持将制表符替换为空格,建议一个制表符=4个空格
- 返回类型,函数名,前几个参数都放在头一行
- 空行不要超过两行