本文章主要记录Google Cpp Guide 内容讲解。
未完成,还在持续更新…
头文件规则
好的头文件
-
头文件需要满足"自给自足",包含它应该包含的所有需要的依赖,即使用者使用时不需要再去包含其他头文件才能去使用当前头文件
-
头文件应该有具体的作用域,避免在全局命名空间内定义全局符号;
-
使用
#define #ifndef
等包含头文件,防止重复的包含; -
尽可能的避免使用前向声明
前向声明的好处:
a. 加快编译速度,避免不必要的重复编译;
前向声明的坏处:
a. 不使用#include
,使用前向声明可能会给一些自动化工具带来困难,一些工具工作时需要查找并识别定义符号的模块;
b. 一些场景可能会对函数和模板进行前向声明,这样会限制库的所有者进行一些兼容修改等(对类型的前向声明一般不会有这种问题);
//函数前向声明
int add(int a,int b);
c. 在std名字空间声明前向符号可能会导致未定义的行为,因为随着库和标准的更新而发生变化,可能会导致前向声明的符号与实际定义的符号不匹配,导致未定义错误; -
当函数很小的时候才将函数定义为内联函数(inline,无堆栈开销),如函数行数小于等于10行。
原因:
这与编译器有关,编译器将一个函数变为内联函数,需要将内联函数的函数体嵌入到每个调用该函数的地方,会有较多的重复代码,如果该函数特别大,那将会在每个调用点展开,会导致整个程序的体积增大,使得编译器的工作量也大,甚至可能会降低程序的效率(程序所需的空间大,增加了程序的代码量,可能导致代码段没有装入到CPU缓存,导致CPU处理效率降低)。 -
注意头文件的包含顺序,一般为相关头文件、C 系统头文件、C++ 标准库头文件、其他库的头文件、您项目的头文件,不同头文件之间用空格隔开;
不好的头文件
- 每个头文件包含多个其他头文件,造成递归或者深嵌套的问题;
- 全局符号过多,可见性和可重用性降低;
- 定义了符号,但是没有包含其所有的依赖文件。
作用域
Namespaces
除少数情况下,我们应该将代码放到命名空间中去,在大型项目中可以很好的防止符号冲突;
内联名字空间
内联名字空间的优先级高于普通的命名空间。不需要使用using语句就可以直接在外层命名空间使用该命名空间内部的内容,而且无需使用命名空间前缀。
Google Cpp Guide规定禁止使用内联函数。
匿名名字空间
一般将.cpp中定义的不需要外部引用的函数放在未命名的名字空间中,也可以使用static修饰函数。
namespace
{
void foo()
{
return;
}
}
局部变量
- 定义即初始化,而不是先定义再初始化
//bad
int i;
i = foo();
//good
int i = foo();
- 变量声明到使用之间尽量不要间隔太多东西(再不影响逻辑正确的情况下)
//bad
int ret = foo();
//more code ...
fun(ret);
//good
int ret = foo();
fun(ret);
//mode code ...
- 优先使用大括号对变量进行初始化,而不是一一赋值
//bad
std::vector<int> vecs;
vecs.push_back(1);
vecs.push_back(1);
//good
std::vector<int> vecs = {1,2};
- while,for循环使用的变量应该在相应语块中定义,而不是前语句前定义,以限制其使用的范围
while (const char* p = strchr(str, '/')) str = p + 1;
一种特殊情况,需要避免对象的重复构造与析构
// Inefficient implementation:
for (int i = 0; i < 1000000; ++i) {
Foo f; // My ctor and dtor get called 1000000 times each.
f.DoSomething(i);
}
Foo f; // My ctor and dtor get called once each.
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
静态和全局变量
禁止使用具有静态存储持续时间的对象(static,extern,thread_local),除非他们是可轻易被析构的(trivially destructibl 平凡析构)。即这些对象的构造和析构函数什么什么也不做,其析构函数可以由编译器生成。
使用非平凡析构的静态变量时,可能会产生一些难以发现的错误。因为全局变量的构造函数,析构函数以及初始化操作的调用顺序知识被部分规定,每次生成可能会有变化,引发难以发现的bugs.
线程局部变量
thread_local本质是一个线程安全的全局变量,
Class
构造函数职责
- 构造函数中不允许调用虚函数(保证构造函数执行完成来保证对象的完整状态,防止发生未定义行为);
- 如果代码逻辑允许,直接终止程序是一个比较合适的错误处理方式,需要避免有无法提示错误初始化的动作;
- 可以考虑加入Init()方法或者工厂函数,将有实际意义的初始化操作放在Init()函数中,构造函数中进行一些"无意义"初始化操作,甚至可以加入如bool IsValid()来判断对象状态。
隐式类型转换
- 对于转换运算符和单参数构造函数,使用explicit关键字;
移动构造和移动赋值
- 如果你设计的类需要,就添加相应的方法支持,否则禁用掉;
- 如果不需要,显式的在public作用域中使用=delete进行删除禁用。
class与struct
仅当只有数据时使用struct,其他情况都使用class。
C++中class与struct几乎一样,只有如默认访问限定符等不一样。
struct 与 pair & tuple
当成员名称可以更有意义,更有可读性时,优先使用struct代替pair和tuple,而不是使用.first,.second,这样显得等模糊。
继承
优先使用组合的方式替代继承。如果需要使用继承,则使用public继承。
继承的优点是代码复用,缺点是子类是对父类的延展,对于代码的理解可能会便困难,子类不能重写父类的非虚函数,过深的继承关系也可能带来性能问题。
运算符的重载
明智的去重载运算符,而不是无脑的进行。
优点:
运算符的重载可以使得用户定义的类型的一些行为就像编译器内置类型一样的丝滑。
缺点:
- 重载的调用点查找困难,Equals()函数比operator==更直观,CopyFrom()比operator=()也更加的直观;
- 会有一些意想不到副作用,如重载&的类不能被前置声明;
- 理解有误差 容易出错。
访问限定符
类的数据成员应该用private修饰,除非是常成员变量。
声明顺序
次序为public,protected,private,成员函数在成员变量之前
每一块中的声明顺序:
- 类型和类型重定义,如typedef,using;
- 静态常量
- 工厂方法
- 构造函数和赋值运算符
- 析构函数
- 所有的其他函数(static和非static函数以及friend)
- 所有其他数据成员(静态和非静态)
函数
输入与输出
- 优先使用返回值返回参数,而不是传出参数;
- 优先按值返回
- 避免返回指针,除非他确实需要这样;
- 在参数列表中,输入参数排在输出参数之前;
- 有时需要常引用类型的参数(降低复制成本,且函数内不允许修改),而有时该避免定义该类型的函数;
写精干的函数
写小且功能集中单一的函数,建议一个函数不要超过40行。
函数重载
仅在函数输入参数类型不同,功能相同时使用重载函数,包括构造函数,不要使用函数重载模仿缺省函数参数。
默认参数
如果能保证默认值始终具有相同的值,那么在非虚函数上允许使用默认参数。
返回值后置
仅当普通的语法实现某些功能不可用或者可读性差时才才使用后置。
lambada函数是需要的。
int foo(int x);
auto foo(int x) -> int
Google-Specific Magic
所有权与智能指针
对于内存为动态分配的对象,最好拥有单一且固定的所有者。最好使用智能指针转移所有权。
std::shared_ptr 共享所有权,资源将有最后一个使用者删除
std::unique_pt 独占所有权,