虽然在 YuleFox、Yang.Y、acgtyrant等诸位大佬的努力下,Google 开源项目风格指南——中文版已经大幅减轻了我们的学习成本,但是文中部分专业的术语或者表达方式还是让过于萌新的读者(比如说我)在流畅的阅读过程中突遇卡顿,不得不查阅各种资料理清原委,这也是写学习笔记的初衷。
Self-contained(自给自足) :所有头文件要能够自给自足。换言之,include
该头文件之后不应该为了使用它而再包含额外的头文件。举个例子:
// a.h
class MyClass {
MyClass(std::string s);
};
// a.cc
#include “a.h”
int main(){
std:string s;
MyClass m(s);
return 0;
}
a.cc
文件会因为没有 #include
而无法成功编译。但是,本质原因是因为 a.h
文件用到了 std::string
却没有 #include
,因此 a.h
文件没有做到自给自足 (Self-contained )。
特殊情况
.h
文件声明并定义了一个模板或内联函数。那么凡是有用到模版或内联函数的 .cc
文件,就必须包含该头文件(不是.cc
文件对应的.h
是否包含该头文件的问题了),否则程序可能会在构建中链接失败。.cc
文件里。头文件保护旨在防止头文件被多重包含,当一个头文件被多次 include
时,可能会出现以下问题:
为保证唯一性,通常有两种解决方法:
例如,项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_ // if not defined,如果FOO_BAR_BAZ_H_没有被宏定义过
#define FOO_BAR_BAZ_H_ // 那么对FOO_BAR_BAZ_H_进行宏定义
...
#endif // if范围结束
#program once 较 #ifndef 出现的晚,因此兼容性会较 #ifndef 差一些,但性能会好一些。
「前置声明」(forward declaration
)是类、函数和模板的纯粹声明,没有定义。
优点:
#include
。极端情况下,用前置声明代替 #include
甚至会改变代码的含义:// b.h:
struct B {};
struct D : B {};
// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); } // calls f(B*)
如果 #include
被 B
和 D
的前置声明替代,此时由于没有函数定义,D
继承自B
这一关系未显现,因此调用 test
函数时会调用f(void*)
。
实测:
10
行的函数;switch
语句的函数常常是得不偿失 ;.cc
文件里。这样可以使头文件的类保持精炼,也很好地贯彻了声明与定义分离的原则。.h
文件中,如果成员函数比较短,也直接放在 .h
中(让它成为内联函数)。路径
项目内头文件应按照项目源代码目录树结构排列,避免使用 UNIX
特殊的快捷目录: .
(当前目录) 或 ..
(上级目录)。例如,google-awesome-project/src/base/logging.h
应该按如下方式包含:
#include "base/logging.h"
顺序
dir/foo.cc
的主要作用是实现或测试 dir2/foo2.h
的功能,foo.cc
中包含头文件的次序如下:
dir2/foo2.h
).h
文件(比如OpenGL和Qt).h
文件按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。
例外:有时,部分 include
语句需要条件编译(conditional includes
),这些代码可以放到其它 includes
之后。
#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11.
#ifdef LANG_CXX11
#include
#endif // LANG_CXX11
内容
symbols
) 被哪些头文件所定义,就应该包含(include
)哪些头文件,即使这些头文件可能已经被已经包含(include
)的头文件包含(include
)了。举例:
bar.h
中的某个符号,哪怕所包含的 foo.h
已经包含了 bar.h
,也照样得包含 bar.h
, 除非 foo.h
有明确说明它会自动提供 bar.h
中的 symbol
。cc
文件所对应的「相关头文件」已经包含的,就不用再重复包含进 cc
文件里面了,就像 foo.cc
只包含 foo.h
就够了,不用再管 foo.h
所包含的其它内容。.cc文件
内使用 匿名命名空间
或 static
声明,但不要在 .h文件
中这么做;命名空间别名
除非显式标记内部命名空间使用。using
指示(using-directive);namespace a {
...code for a... // 左对齐 不缩进
} // namespace a
/* 即使是匿名空间也需要在最后做出注释 */
namespace {
...
} // namespace
- 声明嵌套命名空间时,每个命名空间都独立成行。
namespace foo {
namespace bar { // 不要有额外缩进
} // namespace bar
} // namespace foo
namespace foo {
namespace bar { // 不要有额外缩进
} // namespace bar
} // namespace foo
.cc
文件中使用它,可在 .cc
文件中使用 匿名命名空间 或 static链接关键字 限定其作用域。如:// .cc 文件内
static int Foo() {
...
}
if
、while
和 for
语句的变量应当在这些语句中声明,以此将变量的作用域限制在语句中。有一个例外,如果变量是一个对象,每次进入作用域都要调用其构造函数,每次退出作用域都要调用其析构函数,这会导致效率降低。
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
在循环作用域外面声明这类变量要高效的多:
Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
以下提及的 静态变量 泛指 静态生存周期的对象,包括:全局变量、静态变量、静态类成员变量以及函数静态变量。
不定顺序问题
同一个编译单元内初始化顺序是明确的,静态初始化优先于动态初始化(如果动态初始化未被提前),初始化顺序按照声明顺序进行,销毁则逆序。但是不同的编译单元之间初始化和销毁顺序属于未明确行为 (unspecified behaviour)。
同时,静态变量在程序中断时会被析构,无论所谓中断是从main()返回还是对exit()的调用。析构顺序正好与构造函数调用的顺序相反。但如第一段所言,既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑,此时其它线程试图访问它且失败;再比如,一个静态string变量也许会在一个引用了它的其它变量析构之前被析构掉。
POD
:即 int
、char
和 float
,以及 POD
类型的指针、数组和结构体。即完全禁用 vector
(可以使用 C 数组替代) 和 string
(可以使用 const char []
替代)。class
类型的静态变量,可以考虑在 main()
函数或 pthread_once()
内初始化一个指针且永不回收。注意只能用 raw
(原始) 指针,别用智能指针,毕竟后者的析构函数涉及到不定顺序问题。static
变量:因为它的生命周期不跟随类的生命周期,因此会导致难以发现的 bug
。不过 constexpr
变量除外,毕竟它们又不涉及动态初始化或析构。bool IsValid()
或类似这样的机制才能检查出来,然而这是一个十分容易被疏忽的方法。Init()
),不过这就要求严格遵循预设的调用顺序:构造函数——进行可能失败初始化的函数——使用类内成员的行为。举个例子,假定A
的初始化可能会出错,因此使用命名空间Initialization
内的函数InitializationA
来初始化A
(方便捕捉错误信息),假设B
中有A
这样的初始化可能会出错的成员,也有POD
成员,那么POD
成员的初始化可以放在构造函数中执行,而A
这样的初始化可能会出错的成员必须放在一个单独的成员函数InitMember
中去执行,并捕捉错误信息且返回。在真正构造B
的对象时,必须调用构造函数
+InitMember
才能完整实现构造行为,之后才能使用B
中的成员:
class A{
...// 类内详情不表
};
namespace Initialization{
std::string InitializationA(){
std::string errorInfo;
...// 执行 成员a 的初始化,并且将可能出现的错误信息保存到变量errorInfo中并输出
return errorInfo;
}
} // namespace Initialization
class B{
A a;
... // 其他初始化有可能失败的成员
int bi;
public:
B(int bi_):bi(bi_){} // 构造函数中不执行B类成员a的初始化,因为a的初始化可能出错
std::string InitMember(){
if (!(Initialization::InitializationA().empty())) {
// 如果返回值不为空说明初始化 a 失败
return Initialization::InitializationA(); // 返回错误说明
}
... // 执行其他可能失败的初始化
return NULL; // 所有可能失败的初始化都成功了,返回空
}
void useA(){
...// 使用 成员a 的代码
}
};
int main(){
// 调用顺序
B b(3); // 构造函数
b.InitA(); // 初始化 a
if(b.InitMember().empty()) {
b.useA(); // 有可能失败的初始化都成功才能使用对应的成员
}
return 0;
}
不要定义隐式类型转换。对于转换运算符和单参数构造函数,请使用explicit
关键字。否则会有类型转换二义性的问题。
explicit
,因为它们并不执行类型转换。explicit
。初始化器列表构造函数(接受一个 std::initializer_list
作为参数的构造函数)也应当省略 explicit
,以便支持拷贝初始化(例如 MyType m = {1, 2};
)。如果类型不需要支持拷贝/ 移动,就把隐式产生的拷贝和移动函数禁用。因为某种情况下(如:通过传值的方式传递对象)编译器会隐式调用拷贝和移动函数。
禁用隐式产生的拷贝和移动函数有两种方法:
public
域中通过=delete
:class A{
public:
A() = default; // 使用合成的默认构造函数
// class A is neither copyable nor movable.
A(const A&) = delete; // 阻止拷贝
A &operator=(const A&) = delete; // 阻止赋值
};
private
但不定义的方法 来起到新标准中 =delete
的作用,此时试图使用该种函数的用户代码将在编译阶段被标记为链接错误。总结
std::unique_ptr
。public virtual Clone()
和一个protected
的拷贝构造函数以供派生类实现。struct
用来定义包含数据的被动式(等待初始化或赋值)对象,也可以包含相关的常量,但除了存取数据成员之外,没有别的函数功能。并且存取功能是通过直接访问位域实现的,而非函数调用。除了构造函数、析构函数、Initialize()
、Reset()
、Validate()
等类似的用于设定数据成员的函数外,不能提供其它功能的函数。
class
更适合。如果拿不准,就用 class
。STL
保持一致,对于仿函数等特性可以不用 class
而是使用 struct
。组合 > 实现继承 > 接口继承 > 私有继承
public
的。如果想使用私有继承,可以把基类的实例作为类内成员。is-a
的情况下才实现继承,has-a
的情况下使用组合。即如果 Bar
的确 “是一种” Foo
,Bar
才能继承 Foo
。override
或 (较不常用的) final
关键字显式地进行标记。早于C++11的代码可能会使用 virtual
关键字作为不得已的选项。真正需要用到多重实现继承的情况少之又少。
多重继承应遵循:最多只有一个基类是非抽象类;其它基类都是以 Interface
为后缀的纯接口类。
纯接口:
Interface
为后缀(不强制)。class Foo_Interface {
... // 类的具体细节
};
protected
。Interface
为后缀的类继承而来。为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(因此析构函数不能是纯虚函数)。
尽量不要重载运算符,也不要创建用户定义字面量。不得不使用时提供说明文档。
a < b
能够通过编译而 b < a
不能的情况,这是很让人迷惑的。operator""
。&&
、||
、,
、一元运算符 &
。重载一元运算符&
会导致代码具有二义性。重载&&
、||
和,
会导致运算顺序和内建运算的顺序不一致。==
、=
和 <<
而不是 Equals()
、CopyFrom()
和 PrintTo()
。但是,不要只是为了满足函数库需要而去定义运算符重载。比如说,如果类型没有自然顺序,而又要将它们存入 std::set
中,最好还是定义一个自定义的比较运算符(比较函数?)而不是重载 <
。|
要作为位或
或逻辑或
来使用,而不是作为 shell
中的管道。头文件
中、.cc
中和命名空间
中。这样做无论类型在哪里都能够使用定义的运算符,并且最大程度上避免了多重定义的风险。<
,那么请将所有的比较运算符都进行重载,并且保证对于同一组参数,<
和 >
不会同时返回 true
。private
,除非是 static const
类型成员。public:
开始,后跟 protected:
,最后是 private:
;public
外,其他关键词前要空一行。如果类比较小的话也可以不空。但是关键词后不要保留空行。typedef
、using
和嵌套的结构体与类)如果函数超过 40
行,可以思索一下能不能在不影响程序结构的前提下对其进行分割。
所有按引用传递的参数必须加上 const
。
int foo(int *pval)
。int foo(int &val)
。引用参数的优点
(*pval)++
这样丑陋的代码。引用参数不使用的情况
有时候,在输入形参中用 const T*
指针比 const T&
更明智。比如:
double a = 3.0;
double *p = &a;
double &b = p; // 引用不能绑定地址的引用(指针本身)
double &b = *p; // 引用可以绑定指针指向的对象
在同一个作用域下,对于相同的函数名:
都可以形成函数的重载。
不形成重载。
缺点
结论
AppendString()
和 AppendInt()
等而不是一口气重载多个Append()
。std::vector
作为形参以便使用者可以用 列表初始化 传入实参。缺点
// Before change.
void func(int a);
func(42);
void (*func_ptr)(int) = &func;
// After change.
void func(int a, int b = 10);
func(42); // Still works.
void (*func_ptr)(int) = &func; // Error, wrong function signature.
/* 此外把自带缺省参数的函数地址赋值给指针时,会丢失缺省参数信息。*/
void optimize(int level=3);
void (*fp)() = &optimize; // 即使参数是缺省的,也不可以省略对类型的说明
// 错误 error: invalid conversion from ‘int (*)(int)’ to ‘int (*)()’
void (*fpi)(int) = &optimize; // 正确
void f(int n = counter++);
这样的代码。int my_rand() {
srand(time(NULL));
int ra = rand() % 100;
return ra;
}
void fun(int a, int b = my_rand()) { // 缺省实参是表达式
cout << "a = " << a << " b= " << b << endl;
}
#include
using namespace std;
class A {
public:
virtual void Fun(int number = 10)
{
cout << "A::Fun with number " << number;
}
};
class B: public A {
public:
virtual void Fun(int number = 20)
{
cout << "B::Fun with number " << number << endl;
}
};
int main() {
B b;
A &a = b;
a.Fun(); // 输出结果是 B::Fun with number 10
return 0;
}
输出结果是B::Fun with number 10
。调用虚函数Fun
时,A类
指针a
指向了B类
对象b
,这就导致缺省值静态绑定了A类
成员函数Fun
的缺省值number = 10
,而函数内容动态绑定了指向对象B类
的成员函数Fun
。
结论
可以在以下情况使用缺省参数:
.cc
文件里的静态函数或匿名空间函数,毕竟他们的生命周期被限定在局部文件里。// b、c、d 作为变长数组,维度根据 gEmptyAlphaNum 指定
string StrCat(const AlphaNum &a,
const AlphaNum &b = gEmptyAlphaNum,
const AlphaNum &c = gEmptyAlphaNum,
const AlphaNum &d = gEmptyAlphaNum);
按值返回 > 按引用返回。 避免返回指针,除非可以为空。
void foo (int input1, double input2, int &output){
output = output - (input1 + input2);
// 函数外继续使用output对应的实参进行后续操作即可
// 什么是纯输出参数呢?
// 个人理解就是 outpet 不参与类型运算,仅接受输入参数运算结果的情况吧
// 即上面的语句变更为 output = input1 + input2;
}
函数参数的类型选择
const T*
则说明有特殊情况【详见4.1】,所以应在注释中给出相应的理由。)std::optional
来表示按值输入;const指针
来表示其他输入;非const指针
来表示输出参数。std::optional
详见两篇博客:
int foo(int x);
auto
关键字,在参数列表之后说明后置返回类型:auto foo(int x) -> int;
优点
// 后置
template <class T, class U> auto add(T t, U u) -> decltype(t + u);
// 前置
template <class T, class U> decltype(declval<T&>() + declval<U&>()) add(T t, U u);
动态分配对象的所有者是一个对象或函数,所有者负责确保当前者无用时就自动销毁前者。
std::unique_ptr
离开作用域时(其本身被销毁),对象就会被销毁。std::unique_ptr
不能被复制,但可以把所指对象移动(move
)给新所有者。std::shared_ptr
同样表示动态分配对象的所有权,但可以被共享和复制;对象的所有权由所有复制者共同拥有,最后一个复制者被销毁时,对象也会随着被销毁。const对象
来说,智能指针简单易用,也比深拷贝高效。std::shared_ptr
)时候,才该使用std::shared_ptr
。std::forward
。std::unique_ptr
,std::move
是必需的。变长数组中的“变”指的是:在创建数组时,可以使用变量指定数组的维度。而不是可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。
alloca()
不是标准 C++ 的组成部分(C99中变长数组作为函数形参)。alloca()
根据数据大小动态分配堆栈内存,会引起难以发现的内存越界 bug
: “在我的机器上运行的好好的,发布后却莫名其妙的挂掉了。”private
、 protected
成员声明为 public
,使用友元是更好的选择。尤其是只允许另一个类访问该类的私有成员时。下面列举两个情景:
FooBuilder
声明为 Foo
的友元,以便 FooBuilder
正确构造 Foo
的内部状态。friend
实际上只对函数/类赋予了对其所在类的访问权限,并不是有效的声明语句。所以除了在头文件类内部写 friend
函数/类,还要在类作用域之外正式地声明一遍,最后在对应的.cc
文件加以定义。优点
factory function
,即「简单工厂模式」)或 Init()
方法代替异常,但是前者要求在堆栈分配内存,后者会导致构造函数创建的实例处于“无效”状态。(调用Init()
方法真正完成对类内成员的构造后才能叫做“有效”)缺点
throw
语句时,必须检查所有调用点。要么让所有调用点统统具备最低限度的异常安全保证,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。举例:f()
调用 g()
、g()
又调用 h()
、且 h
抛出的异常被 f
捕获,忽略了g
。结论
RTTI 允许程序员在运行时识别 C++ 类对象的类型。它通过使用 typeid
或者 dynamic_cast
完成。
RTTI 有合理的用途但是容易被滥用,因此在使用时请务必注意。
dynamic_cast
。switch
语句散布在代码各处,不方便后续修改。基于类型的判断树:if (typeid(*data) == typeid(D1)) {
...
} else if (typeid(*data) == typeid(D2)) {
...
} else if (typeid(*data) == typeid(D3)) {
...
}
一旦在类层级中加入新的子类,像这样的代码往往会崩溃。而且,一旦某个子类的属性改变了,很难找到并修改所有受影响的代码块。
不要使用 C 风格类型转换,而应该使用 C++ 风格。详见。
printf()
和 scanf()
。printf + read/write
代替。++i
)通常要比后置(i++
)效率更高。因为后置自增/自减会对表达式的值i
进行一次拷贝。如果i
是迭代器或其他非数值类型,拷贝的代价是比较大的。++i
)自增 / 自减。在有需要的情况下都要使用 const
,有时改用 C++11 推出的 constexpr
更好。
注意初始化 const
对象时,必须在初始化的同时值初始化。
const 用法
const
限定符表明该函数不会修改类成员变量的状态:class Foo {
int Bar(char c) const;
};
const 使用场景
constexpr
详见 constexpr和常量表达式
定义了 int16_t
、uint32_t
、int64_t
等整型,在需要确保整型大小时可以使用它们代替 short
、unsigned long long
等。在合适的情况下,推荐使用标准类型如 size_t
和 ptrdiff_t
。uint32_t
等无符号整型,除非是在表示一个位组而不是一个数值,或是需要定义二进制补码溢出。尤其是不要为了指出数值永不为负而使用无符号类型,而应使用断言。size
),确保接收变量的类型足以应付容器各种可能的用法。拿不准时,类型越大越好。integer promotions
),比如int
与unsigned int
运算时,前者被提升为unsigned int
可能导致溢出。尽量以内联函数,枚举和常量代替宏。
这样代替宏
const
变量代替。#define
防止头文件重包含当然是个特例)。如果无法避免使用宏
#
字符串化,用 ##
连接等等。如果非要用宏,请遵守:
.h
文件中定义宏;#define
,使用后要立即 #undef
;#undef
,也可以选择一个不会冲突的名称;##
处理函数,类和变量的名字。整数用 0
,实数用 0.0
,字符 (串) 用 '\0'
。
对于指针(地址值),到底是用 0、NULL 还是 nullptr?
nullptr
;NULL
,毕竟它看起来像指针。实际上,一些 C++ 编译器对 NULL
的定义比较特殊,可以用来输出警告,比如 sizeof(NULL)
就和 sizeof(0)
不一样。尽可能用 sizeof(varname)
代替 sizeof(type)
:
Struct data;
// 如果要使用 sizeof
memset(&data, 0, sizeof(data)); // 这样做
memset(&data, 0, sizeof(Struct)); // 而不是
sizeof(varname)
是因为当代码中变量类型改变时会自动更新。sizeof(type)
处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量名就不合适了。if (raw_size < sizeof(int)) {
LOG(ERROR) << "compressed record not big enough for count: " << raw_size;
return false;
}
只要可读性好就可以用 auto
绕过繁琐的类型名,但别用在局部变量之外的地方。
缺点
auto
和 const auto&
的不同之处,否则会复制错东西。normally-invisible proxy types
)使用auto
会有意想不到的陷阱。比如 auto
和 C++11 列表初始化的合体:auto x(3); // 圆括号。
auto y{3}; // 大括号。
x
的类型是 int
,y
的类型则是 std::initializer_list
。总结
auto
还可以和 C++11 特性「尾置返回类型」一起用,不过后者只能用在 lambda
表达式里。详见 列表初始化。
适当使用 lambda
表达式。禁用默认 lambda
捕获,所有捕获都要显式写出来。
[=](int x) {return x + n;} // 差,可读性不高
[n](int x) {return x + n;} // 好,读者一眼看出 n 是被捕获的值。
C++11 首次提出Lambdas
,还提供了一系列处理函数对象的工具,比如多态包装器 std::function
。Lambda
表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传,例如:
std::sort(v.begin(), v.end(), [](int x, int y) {
return Weight(x) < Weight(y);
});
优点
Lambdas
最简易,可读性也好。Lambdas
、std::functions
和std::bind
可以搭配成通用回调机制;写接收有界函数为参数的函数也很容易了。缺点
结论
lambda
的尾置返回类型,就像auto
。因为模板的维护成本较高,因此最好只用在少量的基础组件、基础数据结构上,这样模版的使用率高,维护模版就是值得的。(如果一个东西用得少,成本还高你会买吗?)
如果无法避免使用模版编程
C++11 以下特性能不用就不要用:
,因为它涉及一个重模板的接口风格。
和
头文件,因为编译器尚不支持。auto foo() -> int
代替int foo()
。auto
与尾置返回类型一起用的全新编码风格,值得一看。这些约定是Google开发团队遵守的,如果和你的开发团队的规则相冲突,请遵循你的团队的规则。
好的做法:
int price_count_reader; // 无缩写
int num_errors; // "num" 是一个常见的写法
int num_dns_connections; // 人人都知道 "DNS" 是什么
此外,一些特定的广为人知的缩写是允许的,例如用
i
表示迭代变量和用T
表示模板参数。
坏的做法:
int n; // 毫无意义.
int nerr; // 含糊不清的缩写.
int n_comp_conns; // 含糊不清的缩写.
int wgc_connections; // 只有贵团队知道是什么意思.
int pc_reader; // "pc" 有太多可能的解释了.
int cstmr_id; // 删减了若干字母.
my_useful_class.cc
/ my-useful-class.cc
/ myusefulclass.ccmyusefulclass_test.cc
已弃用。(部分词语以符号隔开,部分不隔开,不统一)_unittest
和 _regtest
已弃用。(不以符号开头).cc
结尾,头文件以 .h
结尾,专门插入文本的文件则以 .inc
结尾。/usr/include
下的文件名(即编译器搜索系统头文件的路径)。http_server_logs.h
就比logs.h
要好。定义类时文件名一般成对出现,如foo_bar.h
和foo_bar.cc
。类、结构体、类型定义 (typedef
)、枚举、类型模板参数名称 的每个单词首字母均大写,不包含下划线:
// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;
// 枚举
enum UrlTableErrors { ...
普通变量命名 / 结构体变量
变量(包括函数参数)和数据成员名一律小写,单词之间用下划线连接。
string table_name; // 好 - 用下划线.
string tablename; // 好 - 全小写.
string tableName; // 差 - 混合大小写
// 结构体
struct UrlTableProperties {
string name;
int num_entries;
static Pool<UrlTableProperties>* pool;
};
类数据成员
类的成员变量以下划线结尾。
class TableInfo {
...
private:
string table_name_; // 好 - 后加下划线.
string tablename_; // 好.
static Pool<TableInfo>* pool_; // 好.
};
声明为 constexpr
或 const
的变量,或在程序运行期间其值始终保持不变的,命名时以 k
开头,大小写混合。例如:
const int kDaysInAWeek = 7;
MyExcitingFunction();
my_exciting_member_variable();
set_my_exciting_member_variable();
StartRpc(); // 好的
StartRPC(); // 不好
std
命名空间。由于名称查找规则的存在,命名空间之间的冲突完全有可能导致编译失败。internal
命名空间的代码之间发生冲突。 在这种情况下,请使用文件名使内部名称独一无二(例如frobber.h
,使用websearch::index::frobber_internal
)。通常 不应该 使用宏,如果不得不用,其命名要像枚举命名一样全部大写,使用下划线:
#define ROUND(x) ...
#define PI_ROUNDED 3.0
kEnumName
),但 宏 方式的命名也可以接受。UrlTableErrors/ AlternateUrlTableErrors
是类型,所以要用大小写混合的方式:enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
kErrorMalformedInput,
...
};
enum AlternateUrlTableErrors {
OK = 0,
OUT_OF_MEMORY = 1,
MALFORMED_INPUT = 2,
...
};
.h
和.cc
文件中),此时,描述类用法的注释应当和接口定义放在一起,描述类的操作的注释应当和实现放在一起。// Iterates over the contents of a GargantuanTable.
// Example:
// GargantuanTableIterator* iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
// process(iter->key(), iter->value());
// }
// delete iter;
class GargantuanTableIterator {
...
};
类数据成员
NULL
或 -1
等警戒值,须加以说明。比如:private:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries_;
全局变量
// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;
对于代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。
代码前注释
// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
x = (x << 8) + (*result)[i];
(*result)[i] = x >> 1;
x &= 1;
}
行注释
// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
return; // Error already logged.
如果你需要连续进行多行注释,可以使之对齐获得更好的可读性:
DoSomething(); // Comment here so the comments line up.
DoSomethingElseThatIsLonger(); // Two spaces between the code and the comment.
{ // One space before comment when opening a new scope is allowed,
// thus the comment lines up with the following comments and code.
DoSomethingElse(); // Two spaces before line comments normally.
}
std::vector<string> list{
// Comments in braced lists describe the next element...
"First item",
// .. and should be aligned appropriately.
"Second item"};
DoSomething(); /* For trailing block comments, one space is fine. */
函数参数注释
万不得已时,才考虑在调用点用注释阐明参数的意义。
// 参数意义不明,单独加注释并不是一个好的解决方案
const DecimalNumber product = CalculateProduct(values, 7, false, nullptr);
不如:
// 用变量options接收上面第二、第三个参数
// 并用变量名解释他们的含义,这比为两者添加注释要好
ProductOptions options;
options.set_precision_decimals(7);
options.set_use_cache(ProductOptions::kDontUseCache);
const DecimalNumber product =
CalculateProduct(values, options, /*completion_callback=*/nullptr);
不允许的行为
// Find the element in the vector. <-- 差: 这太明显了!
// 或者下面这样的注释
// Process "element" unless it was already processed.
auto iter = std::find(v.begin(), v.end(), element);
if (iter != v.end()) {
Process(element);
}
if (!IsAlreadyProcessed(element)) {
Process(element);
}
TODO
注释。TODO
注释使用全大写的字符串,在随后的圆括号里写上身份标识和与TODO
相关的 issue
。// TODO([email protected]): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
// TODO(bug 12345): remove the "Last visitors" feature
TODO
是为了在 “将来某一天做某事”,可以附上一个非常明确的时间,或者一个明确的事项。// TODO(bug 12345): Fix by November 2022
// TODO([email protected]): Remove this code when all clients can handle XML responses.
DEPRECATED:
comments)以标记某接口点已弃用。注释可以放在接口声明前,或者同一行。ASCII
字符,使用时必须使用 UTF-8
编码。尽量不将字符串常量耦合到代码中,比如独立出资源文件,这不仅仅是风格问题了;UNIX/Linux
下无条件使用空格,MSVC
的话使用 Tab
也无可厚非;函数参数格式
bool retval = DoSomething(argument1, argument2, argument3);
bool retval = DoSomething(averyveryveryverylongargument1,
argument2, argument3);
if (...) {
DoSomething( // 两格缩进
argument1, argument2, // 4 空格缩进
argument3, argument4);
}
// 或者
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // 4 space indent
Type par_name2,
Type par_name3) {
DoSomething(); // 2 space indent
...
}
int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);
bool retval = DoSomething(scores[x] * y + bases[x], // Score heuristic.
x, y, z);
// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);
函数体格式
// 对于单行函数的实现,在大括号内加上空格,然后是函数实现
void Foo() {} // 大括号里面是空的话, 不加空格.
void Reset() { baz_ = 0; } // 用空格把大括号与实现分开.
void 函数里要不要用 return 语句
从 本讨论 来看return;
比return ;
更约定俗成(事实上cpplint
会对后者报错,指出分号前有多余的空格),且可用来提前跳出函数栈。
Lambda 表达式
&
之间不留空格:int x = 0;
auto add_to_x = [&x](int n) { x += n; };
lambda
就写得和内联函数一样:std::set<int> blacklist = {7, 8, 9};
std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1};
digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {
return blacklist.find(i) != blacklist.end();
}),
digits.end());
下面的示例应该可以涵盖大部分情景:
// 一行列表初始化示范.
return {foo, bar};
functioncall({foo, bar});
pair<int, int> p{foo, bar};
// 当不得不断行时.
SomeFunction(
{"assume a zero-length name before {"}, // 假设在 { 前有没有其他参数
some_other_function_parameter);
SomeType variable{
some, other, values,
{"assume a zero-length name before {"}, // 假设在 { 前有其他参数
SomeOtherType{
"Very long string requiring the surrounding breaks.",
// 非常长的字符串, 前后都需要断行.
some, other values},
SomeOtherType{"Slightly shorter string", // 稍短的字符串.
some, other, values}};
SomeType variable{
"This is too long to fit all in one line"}; // 字符串过长, 因此无法放在同一行.
MyType m = { // 注意了, 您可以在 { 前断行.
superlongvariablename1,
superlongvariablename2,
{short, interior, list},
{interiorwrappinglist,
interiorwrappinglist2}};
// 如果初始值列表能放在同一行:
MyClass::MyClass(int var) : some_var_(var) {
DoSomething();
}
// 如果不能放在同一行,
// 必须置于冒号后, 并缩进 4 个空格
MyClass::MyClass(int var)
: some_var_(var), some_other_var_(var + 1) {
DoSomething();
}
// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(int var)
: some_var_(var), // 4 space indent
some_other_var_(var + 1) { // lined up
DoSomething();
}
// 右大括号 } 可以和左大括号 { 放在同一行
// 如果这样做合适的话
MyClass::MyClass(int var)
: some_var_(var) {}
if 判断句的空格要求
if(condition) // 差 - IF 后面没空格.
if (condition){ // 差 - { 前面没空格.
if(condition){ // 变本加厉地差.
if (condition) { // 好 - IF 和 { 都与空格紧邻.
执行语句只有一句
只有当语句简单并且没有使用 else 子句时允许将简短的条件语句写在同一行来增强可读性:
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();
如果语句有 else
分支则不允许:
// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
if (x) DoThis();
else DoThat();
{} - 大括号的使用
Apple 因为没有正确使用大括号栽过跟头 ,因此除非 条件语句能写在一行,否则一定要有大括号。
布尔表达式
逻辑操作符总位于行尾:
if (this_one_thing > this_other_thing &&
a_third_thing == a_fourth_thing &&
yet_another && last_one) {
...
}
循环语句
空循环体应使用 {}
或 continue
,而不是一个简单的分号:
while (condition) {
// 反复循环直到条件失效.
}
for (int i = 0; i < kSomeNumber; ++i) {} // 可 - 空循环体.
while (condition) continue; // 可 - contunue 表明没有逻辑.
while (condition); // 差 - 看起来仅仅只是 while/loop 的部分之一.
switch 选择语句
如果有不满足 case
条件的枚举值,switch
应该总是包含一个 default
匹配(如果有输入值没有case
去处理,编译器将给出warning
)。如果default
永远执行不到,简单的加条 assert
:
switch (var) {
case 0: { // 2 空格缩进
... // 4 空格缩进
break;
}
case 1: {
...
break;
}
default: {
assert(false);
}
}
指针/引用表达式
int x, *y; // 不允许 - 在多重声明中不能使用 & 或 *
char * c; // 差 - * 两边都有空格
const string & str; // 差 - & 两边都有空格.
函数返回值
return result; // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
another_condition);
=
、()
和 {}
均可:int x = 3;
int x(3);
int x{3};
string name("Some Name");
string name = "Some Name";
string name{"Some Name"};
{...}
用 std::initializer_list 构造函数
初始化出的类型:vector<int> v(100, 1); // 内容为 100 个 1 的向量.
vector<int> v{100, 1}; // 内容为 100 和 1 的向量.
int pi(3.14); // 好 - pi == 3.
int pi{3.14}; // 编译错误: 缩窄转换.
即使位于缩进代码块中,预处理指令也应从行首开始:
// 差 - 指令缩进
if (lopsided_score) {
#if DISASTER_PENDING // 差 - "#if" 应该放在行开头
DropEverything();
#endif // 差 - "#endif" 不要缩进
BackToNormal();
}
// 好 - 指令从行首开始
if (lopsided_score) {
#if DISASTER_PENDING // 正确 - 从行首开始
DropEverything();
# if NOTIFY // 非必要 - # 后跟空格
NotifyClient();
# endif
#endif
BackToNormal();
}