当前位置:   article > 正文

C/C++编程:Google代码规范_google c++代码规范

google c++代码规范

头文件

  • 除了单元测试只有.cc外,其他每一个.cc都应该有一个对应的.h

Self-contained 头文件

  • 就是说要保证你的头文件是自给自足的,一个自足的头文件是一个不依赖于它包含在那里的上下文来正确工作的文件。

举个例子:

----- MyClass.h -----

class MyClass
{
MyClass(std::string s);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
---- MyClass.cpp -----
#include “MyClass.h”
#include
MyClass::MyClass(std::string s)
{}
  • 1
  • 2
  • 3
  • 4
  • 5

我们在.h头文件中,用到了string这个源文件,但是在cpp文件中,我们先声明头文件,最后会出现string没有包含这个错误,因为头文件不是自给自足的。只有#include 放在#include "MyClass.h"才行,所以为了避免这种错误,我们需要使头文件满足自给自足这个条件。所以我们的头文件写完必须独立运行编译一下看看是否成功,为此,我们需要在头文件中包含其他所有的文件,在这个例子中,myclass中应该包含string这个文件。

#define 保护避免多重包含

  • 所有头文件都应该有 #define 保护来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_ .
  • 为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
  • 1
  • 2
  • 3
  • 4

#include 的路径及顺序

  • (1)项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 . (当前目录) 或 … (上级目录).
    • 包含文件的名称使用 . 和 … 虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰
    • 例如, google-awesome-project/src/base/logging.h 应该按如下方式包含:
#include "base/logging.h"
  • 1
  • (2) 又如, dir/foo.cc 或 dir/foo_test.cc 的主要作用是实现或测试 dir2/foo2.h 的功能,
    • foo.cc 中包含头文件的次序如下:
      • dir2/foo2.h (优先位置, 详情如下)
      • C 系统文件
      • C++ 系统文件
      • 其他库的 .h 文件
      • 本项目内 .h 文件
    • 这种优先的顺序排序保证当 dir2/foo2.h 遗漏某些必要的库时, dir/foo.cc 或 dir/foo_test.cc 的构建会立刻中止。
    • 在 #include 中插入空行以分割相关头文件, C 库, C++ 库, 其他库的 .h 和本项目内的 .h 是个好习惯。
    • 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在 “最需要编译”的地方编译
    • 举例来说, google-awesome-project/src/foo/internal/fooserver.cc 的包含次序如下:
#include "foo/public/fooserver.h" // 优先位置

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • (3)dir/foo.cc 和 dir2/foo2.h 通常位于同一目录下 (如 base/basictypes_unittest.cc 和 base/basictypes.h), 但也可以放在不同目录下.
  • (4)按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。
  • (5)您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,前置声明 (forward declarations) 情况除外。
    • 比如您要用到 bar.h 中的某个符号, 哪怕您所包含的 foo.h 已经包含了 bar.h, 也照样得包含 bar.h, 除非 foo.h 有明确说明它会自动向您提供 bar.h 中的 symbol.
    • 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 foo.cc 只包含 foo.h 就够了,不用再管后者所包含的其它内容。
    • 有关例外:有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:
#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

尽可能地避免使用前置声明

前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;

  • 原则:尽量避免前置声明,使用#include包含需要的头文件即可。
    • 尽量避免前置声明那些定义在其他项目中的实体
    • 函数:总是使用 #include.
    • 类模板:优先使用 #include.

作用域

作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率.

命名空间

  • 鼓励在 .cc 文件内使用匿名命名空间或 static 声明
    • 在 .cc 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 static 。但是不要在 .h 文件中这么做。
    • 所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为 static 拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。
    • 匿名命名空间的声明和具名的格式相同,在最后注释上 namespace :
namespace {
...
}  // namespace
  • 1
  • 2
  • 3
  • 使用具名命名空间时, 其名称可基于项目名或相对路径.
  • 避免直接使用 using 关键字污染命名空间;不应该使用using 指示 引入整个命名空间的标识符号。
/ 禁止 —— 污染命名空间
using namespace foo;
  • 1
  • 2
  • 禁止使用内联命名空间(inline namespace)。
  • 不要在命名空间 std 内声明任何东西, 包括标准库的类前置声明.
    • 在 std 命名空间声明实体是未定义的行为, 会导致如不可移植. 声明标准库下的实体, 需要包含对应的头文件.
  • 使用时,请在命名空间的最后注释出命名空间的名字。
namespace X {
inline namespace Y {
void foo();
}  // namespace Y
}  // namespace X
  • 1
  • 2
  • 3
  • 4
  • 5
// .h 文件
namespace mynamespace {

// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
    public:
    ...
    void Foo();
};

} // namespace mynamespace
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
// .cc 文件
namespace mynamespace {

// 函数定义都置于命名空间中
void MyClass::Foo() {
    ...
}

} // namespace mynamespace
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 不要在头文件中使用 命名空间别名 除非显式标记内部命名空间使用。因为任何在头文件中引入的命名空间都会成为公开API的一部分。
// 在 .cc 中使用别名缩短常用的命名空间
namespace baz = ::foo::bar::baz;
  • 1
  • 2
// 在 .h 中使用别名缩短常用的命名空间
namespace librarian {
namespace impl {  // 仅限内部使用
namespace sidetable = ::pipeline_diagnostics::sidetable;
}  // namespace impl

inline void my_inline_function() {
  // 限制在一个函数中的命名空间别名
  namespace baz = ::foo::bar::baz;
  ...
}
}  // namespace librarian
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

非成员函数、静态成员函数和全局函数

使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关.

  • (1)非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内或 static 链接关键字 。 ps: 这条啰啰嗦嗦的,实际中我一般只用static静态函数而非命名空间
    • 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用命名空间
    • 如果你必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名命名空间static 链接关键字 (如 static int Foo() {…}) 限定其作用域.
    • 举例而言,对于头文件 myproject/foo_bar.h
# 推荐
namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
}  // namespace foo_bar
}  // namespace myproject


# 不推荐
namespace myproject {
class FooBar {
 public:
  static void Function1();
  static void Function2();
};
}  // namespace myproject
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • (2)公文版本:定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内.
    • 感觉好像没有什么用,一句话:类的静态方法应当和类的实例或静态数据紧密相关.
  • (3)尽量不用全局函数

局部变量

  • (1)将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
    • C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好.
    • 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值.
    • 特别是,应使用初始化的方式替代声明再赋值
// bad
int i;
i = f(); // 坏——初始化和声明分离

vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);

// ok
int j = g(); // 好——初始化时声明
vector<int> v = {1, 2}; // 好——v 一开始就初始化
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • (2)属于 if, while 和 for 语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了,举例而言:
while (const char* p = strchr(str, '/')) str = p + 1;
  • 1
  • (3)有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低.在循环作用域外面声明这类变量要高效的多:
// ------------------低效的实现------
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

静态和全局变量

静态生存周期的对象,即包括了全局变量,静态变量静态类成员变量和函数静态变量,都必须是POD类型以及 POD 类型的指针、数组和结构体,完全禁用 vector (使用 C 数组替代) 和 string (使用 const char [])。

如果你确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,否则析构会出问题

  • (1)禁止使用类的static变量

    • 由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。
    • 不过 constexpr 变量除外,毕竟它们又不涉及动态初始化或析构。
  • (2)静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug.

    • 为什么说它是不明确?
      • 同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。
      • 不同的编译单元之间初始化和销毁顺序属于未明确行为 (unspecified behaviour)。
    • 所以禁用类类型的全局变量不允许用函数返回值来初始化 POD 变量,除非该函数(比如 getenv() 或 getpid() )不涉及任何全局变量。
    • 函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。
  • (3)用 quick_exit() 来代替 exit() 并中断程序

    • 全局和静态变量在程序中断时会被析构,无论所谓中断是从 main() 返回还是对 exit() 的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。
    • 比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。
    • 所以我们应该用 quick_exit() 来代替 exit() 并中断程序
      • quick_exit() 会执行任何析构,也不会执行 atexit() 所绑定的任何 handlers.
      • 如果您想在执行 quick_exit() 来中断时执行某 handler(比如刷新 log),您可以把它绑定到 _at_quick_exit().
  • (4)尽量不用全局变量

  • (5) 多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致的 bug.

嵌套类

嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要 public;

构造函数的职责

  • 不要在构造函数中调用虚函数
    • 如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现
    • 即使当前没有子类化实现, 将来仍是隐患.
  • 不要在构造函数中尝试报告一个非致命错误
    • 异常是处理构造函数失败的唯一途径
    • 如果代码允许, 直接终止程序是一个合适的处理错误的方式. 否则, 考虑用 Init() 方法或工厂函数,注意前者要求在堆栈分配内存,后者会导致刚创建的实例处于 ”无效“ 状态。
  • 不在构造函数中做太多逻辑相关的初始化
  • 构造函数的地址是无法被取得的, 因此, 举例来说, 由构造函数完成的工作是无法以简单的方式交给其他线程的.

隐式类型转换

  • 请尽量避免隐式类型转换.
    • 对于转换运算符, 请使用 explicit 关键字.
    • 对于单参数构造函数, 请使用 explicit 关键字.
      • 不能以一个参数进行调用的构造函数不应当加上 explicit
      • 接受一个 std::initializer_list 作为参数的构造函数也应当省略 explicit, 以便支持拷贝初始化 (例如 MyType m = {1, 2 } ; ).
    • 拷贝和移动构造函数不应当被标记为 explicit, 因为它们并不执行类型转换.
    • 对于设计目的就是用于对其他类型进行透明包装的类来说, 隐式类型转换有时是必要且合适的. 这时应当联系项目组长并说明特殊情况.
  • 使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等转换方式;
    • 用 static_cast 替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时.
    • 用 const_cast 去掉 const 限定符.
    • 用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换. 仅在你对所做一切了然于心时使用.

可拷贝类型和可移动类型

  • 如果你的类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用
    • 如何支持?
      • 如果让类型可拷贝, 一定要同时给出拷贝构造函数和赋值操作的定义, 反之亦然
      • 如果让类型可移动, 同时移动操作的效率高于拷贝操作, 那么就把(移动构造函数和赋值操作) 也给出定义
      • 如果类型不可拷贝但是可移动,那么把这个类型设置为只可移动并定义移动的两个操作.
    • 如何禁用?如果你的类不需要拷贝 / 移动操作, 请显式地通过在 public 域中使用 = delete 或其他手段禁用之.
    • 为什么要这样做?许多类型都不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理.
  • 为基类提供拷贝 / 赋值操作是有害的
    • 由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类).
    • 如果你的基类需要可复制属性, 请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现.

结构体 VS. 类

  • 仅当只有数据成员时使用 struct, 其它一概使用 class.
  • 为了和 STL 保持一致, 对于仿函数等特性可以不用 class 而是使用 struct.

继承

  • 如果需要继承,请用public继承
    • 继承主要有两种场合:
      • 实现继承:子类继承父类的实现代码
      • 接口继承:类仅继承父类的方法名称
    • 所有继承必须是 public 的
      • 如果想要用private继承,你应该替换成把基类的实例作为成员对象的方式(即尽量用组合而不是继承)
  • 成员:
    • 必要的话, 析构函数声明为 virtual:如果你的类有虚函数, 则析构函数也应该为虚函数.
    • 对于重载的虚函数或虚析构函数, 使用 override, 或 (较不常用的) final 关键字显式地进行标记
    • 对于可能被子类访问的成员函数, 不要过度使用 protected 关键字
    • 注意,数据成员都必须是private的,除非是 static const 类型成员
      • 例外:出于技术上的原因, 在使用 Google Test 时我们允许测试固件类中的数据成员为 protected.

多重继承

  • 多重继承允许子类拥有多个基类.
    • 真正需要用到多重实现继承的情况少之又少.
    • 只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承
    • 为确保它们是纯接口, 这些类必须以 Interface 为后缀

接口

纯接口类是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制)。

  • 只有纯虚函数 (”=0”) 、静态函数、析构函数
    • 接口类不能被直接实例化, 因为它声明了纯虚函数
  • 没有非静态数据成员.
  • 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数
  • 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 protected
  • 如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.

声明顺序

  • 类定义一般应以 public: 开始, 后跟 protected:, 最后是 private:
  • 在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 typedef, using 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它函数, 数据成员.

运算符重载

为降低复杂性, 尽量不重载操作符, 模板

  • C++ 允许用户通过使用 operator 关键字 对内建运算符进行重载定义 , 只要其中一个参数是用户定义的类型.
  • operator 关键字还允许用户使用 operator"" 定义新的字面运算符, 并且定义类型转换函数, 例如 operator bool()。请不要创建用户定义字面量.
  • 如果你定义了一个运算符, 请将其相关且有意义的运算符都进行定义, 并且保证这些定义的语义是一致的. 例如, 如果你重载了 <, 那么请将所有的比较运算符都进行重载, 并且保证对于同一组参数, < 和 > 不会同时返回 true.
  • 不要为了避免重载操作符而走极端. 比如说, 应当定义 ==, =, 和 << 而不是 Equals(), CopyFrom() 和 PrintTo().
  • 不要重载 &&, ||, , 或一元运算符 &. 不要重载 operator"", 也就是说, 不要引入用户定义字面量.

函数

传参/返回值

  • 返回值:

    • 我们倾向于按值返回, 否则按引用返回
    • 避免返回指针, 除非它可以为空.
  • 函数参数:

    • 函数参数列表中, 所有引用参数都必须是 const。除非是特殊要求
void Foo(const string &in, string *out);
  • 1
    • 有时候, 在输入形参中用 const T* 指针比 const T& 更明智. 比如:
      • 可能会传递空指针.
      • 函数要把指针或对地址的引用赋值给输入形参.
    • 在排序函数参数时, 将所有输入参数放在所有输出参数之前. 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前.

编写简短函数

函数应该简单简短,尽量不要超过40行

函数重载

  • 如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append()
  • 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector 以便使用者可以用 列表初始化 指定参数.
  • 为什么要这样做?如果函数单靠不同的参数类型而重载 , 读者就得十分熟悉 C++ 五花八门的匹配规则, 以了解匹配过程具体到底如何. 另外, 如果派生类只重载了某个函数的部分变体, 继承语义就容易令人困惑.

禁止缺省参数

禁止使用缺省函数参数,少数极端情况除外。尽可能改用函数重载。

  • 只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致.
    • 对于虚函数, 不允许使用缺省参数, 因为在虚函数中缺省参数不一定能正常工作.
    • 如果在每个调用点缺省参数的值都有可能不同, 在这种情况下缺省函数也不允许使用. (例如, 不要写像 void f(int n = counter++); 这样的代码.)
  • 在其他情况下, 如果缺省参数对可读性的提升远远超过了缺点的话, 可以使用缺省参数. 如果仍有疑惑, 就使用函数重载.

建议:只为可以缺省的参数提供默认值。比如vector的增长倍数,这种不会影响功能正确性的可以考虑使用。但不要滥用默认参数,不要只是因为——哈哈,这是目前的应用场景,为了少打几个字,我把它们设置为默认值好了。

函数返回类型后置语法

  • 在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前. 只有在必需的时候 (如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法.
  • 但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中, 而多数情况下不鼓励写这样 复杂的模板代码.

C++ 现在允许两种不同的函数声明方式. 以往的写法是将返回类型置于函数名之前. 例如:

int foo(int x);
  • 1

C++11 引入了这一新的形式. 现在可以在函数名前使用 auto 关键字, 在参数列表之后后置返回类型. 例如:

auto foo(int x) -> int;
  • 1

后置返回类型为函数作用域. 对于像 int 这样简单的类型, 两种写法没有区别. 但对于复杂的情况, 例如类域中的类型声明或者以函数参数的形式书写的类型, 写法的不同会造成区别.

后置返回类型是显式地指定 Lambda 表达式 的返回值的唯一方式. 某些情况下, 编译器可以自动推导出 Lambda 表达式的返回类型, 但并不是在所有的情况下都能实现. 即使编译器能够自动推导, 显式地指定返回类型也能让读者更明了.

有时在已经出现了的函数参数列表之后指定返回类型, 能够让书写更简单, 也更易读, 尤其是在返回类型依赖于模板参数时.:

template <class T, class U> auto add(T t, U u) -> decltype(t + u);
  • 1

来自 Google 的奇技

所有权与智能指针

动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权.

  • 如果必须使用动态分配, 那么更倾向于将所有权保持在分配者手中
  • 如果其他地方要使用这个对象, 最好传递它的拷贝, 或者传递一个不用改变所有权的指针或引用
  • 倾向于使用 std::unique_ptr 来明确所有权传递
  • 如果没有很好的理由, 则不要使用共享所有权. 这里的理由可以是为了避免开销昂贵的拷贝操作, 但是只有当性能提升非常明显, 并且操作的对象是不可变的(比如说 std::shared_ptr )时候, 才能这么做. 如果确实要使用共享所有权, 建议于使用 std::shared_ptr .

使用 cpplint.py 检查风格错误

.
cpplint.py 是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报, 但它仍然是一个非常有用的工具. 在行尾加 // NOLINT, 或在上一行加 // NOLINTNEXTLINE, 可以忽略报错.

其他

右值引用

  • 只在定义移动构造函数与移动赋值操作时使用右值引用.
  • 不要使用 std::forward
  • 使用 std::move 来表示将值从一个对象移动而不是复制到另一个对象.

不允许使用变长数组和 alloca().

  • 变长数组和 alloca() 不是标准 C++ 的组成部分. 更重要的是, 它们根据数据大小动态分配堆栈内存, 会引起难以发现的内存越界 bugs: “在我的机器上运行的好好的, 发布后却莫名其妙的挂掉了”.
  • 请改用更安全的分配器(allocator),就像 std::vector 或 std::unique_ptr<T[]>.

允许合理的使用友元类及友元函数.

  • 通常友元应该定义在同一文件内
    • 经常用到友元的一个地方是将 FooBuilder 声明为 Foo 的友元, 以便 FooBuilder 正确构造 Foo 的内部状态, 而无需将该状态暴露出来.
    • 某些情况下, 将一个单元测试类声明成待测类的友元会很方便.
  • 由于友元函数/类并不是类的一部分,自然也不会是类可调用的公有接口,于是主张全集中放在类的尾部,即数据成员之后

不使用 C++ 异常

  • 在现有函数中添加 throw 语句时,您必须检查所有调用点。要么让所有调用点统统具备最低限度的异常安全保证,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。举例,f() 调用 g(), g() 又调用 h(), 且 h 抛出的异常被 f 捕获。当心 g, 否则会没妥善清理好。
  • 还有更常见的,异常会彻底扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。您或许会加一大堆何时何处处理异常的规定来降低风险,然而开发者的记忆负担更重了。
  • 异常安全需要RAII和不同的编码实践. 要轻松编写出正确的异常安全代码需要大量的支持机制. 更进一步地说, 为了避免读者理解整个调用表, 异常安全必须隔绝从持续状态写到 “提交” 状态的逻辑. 这一点有利有弊 (因为你也许不得不为了隔离提交而混淆代码). 如果允许使用异常, 我们就不得不时刻关注这样的弊端, 即使有时它们并不值得.
  • 启用异常会增加二进制文件数据,延长编译时间(或许影响小),还可能加大地址空间的压力。
  • 滥用异常会变相鼓励开发者去捕捉不合时宜,或本来就已经没法恢复的「伪异常」。比如,用户的输入不符合格式要求时,也用不着抛异常。如此之类的伪异常列都列不完。

这一点不是很赞同。对使用 C++ 异常处理应具有怎样的态度?

禁止使用 RTTI(运行时类型识别)

RTTI 允许程序员在运行时识别 C++ 类对象的类型. 它通过使用 typeid 或者 dynamic_cast 完成.

  • . 在单元测试中可以使用 RTTI, 但是在其他代码中请尽量避免

    • 如果程序能够保证给定的基类实例实际上都是某个派生类的实例, 那么就可以自由使用 dynamic_cast.
  • 如果你的代码需要根据不同的对象类型执行不同的行为的话, 请考虑用以下的两种替代方案之一查询类型:

    • 虚函数可以根据子类类型的不同而执行不同代码. 这是把工作交给了对象本身去处理.
    • 如果这一工作需要在对象之外完成, 可以考虑使用双重分发的方案, 例如使用访问者设计模式. 这就能够在对象之外进行类型判断.
  • 为什么要这样规定?

    • 在运行时判断类型通常意味着设计问题. 如果你需要在运行期间确定一个对象的类型, 这通常说明你需要考虑重新设计你的类.
    • 基于类型的判断树是一个很强的暗示, 它说明你的代码已经偏离正轨了。比如下面那样一旦在类层级中加入新的子类, 像这样的代码往往会崩溃. 而且, 一旦某个子类的属性改变了, 你很难找到并修改所有受影响的代码块.:
if (typeid(*data) == typeid(D1)) {
  ...
} else if (typeid(*data) == typeid(D2)) {
  ...
} else if (typeid(*data) == typeid(D3)) {
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

不要去手工实现一个类似 RTTI 的方案. 反对 RTTI 的理由同样适用于这些方案, 比如带类型标签的类继承体系. 而且, 这些方案会掩盖你的真实意图.

只在记录日志时使用流

不要使用流, 除非是日志接口需要. 使用 printf 之类的代替.

使用流还有很多利弊, 但代码一致性胜过一切. 不要在代码中使用流.

前置自增和自减

对于变量在自增 (++i 或 i++) 或自减 (–i 或 i–) 后表达式的值又没有没用到的情况下, 需要确定到底是使用前置还是后置的自增 (自减).

  • 对简单数值 (非对象), 两种都无所谓.
  • 对迭代器和模板类型, 使用前置自增 (自减).

尽量用const

我们强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的 constexpr 更好。

const 变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障; 便于尽早发现错误. 因此, 我们强烈建议在任何可能的情况下使用 const:

  • 如果函数不会修改你传入的引用或指针类型参数, 该参数应声明为 const.
  • 尽可能将函数声明为 const. 访问函数应该总是 const. 其他不会修改任何数据成员, 未调用非 const 函数, 不会返回数据成员非 const 指针或引用的函数也应该声明成 const.
  • 如果数据成员在对象构造之后不再发生变化, 可将其定义为 const.

关键字 mutable 可以使用, 但是在多线程中是不安全的, 使用时首先要考虑线程安全.

const的位置:

  • 我们提倡但不强制 const 在前. 但要保持代码的一致性:推荐用const int* foo而非int const *foo

constexpr

变量可以被声明成 constexpr 以表示它是真正意义上的常量,即在编译时和运行时都不变。函数或构造函数也可以被声明成 constexpr, 以用来定义 constexpr 变量。

在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。

整型

  • C++ 没有指定整型的大小. 通常人们假定 short 是 16 位, int 是 32 位, long 是 32 位, long long 是 64 位.

  • C++ 内建整型中, 仅使用 int. 如果程序中需要不同大小的变量, 可以使用 < stdint.h> 中长度精确的整型。<stdint.h> 定义了 int16_t, uint32_t, int64_t 等整型, 在需要确保整型大小时可以使用它们代替 short, unsigned long long 等

    • 在 C 整型中, 只使用 int. 在合适的情况下, 推荐使用标准类型如 size_t 和 ptrdiff_t.
    • 如果已知整数不会太大, 我们常常会使用 int, 如循环计数. 在类似的情况下使用原生类型 int. 你可以认为 int 至少为 32 位, 但不要认为它会多于 32 位
    • 如果需要 64 位整型, 用 int64_t 或 uint64_t.
    • 对于大整数, 使用 int64_t
    • 不要使用 uint32_t 等无符号整型, 除非你是在表示一个位组而不是一个数值, 或是你需要定义二进制补码溢出. 尤其是不要为了指出数值永不会为负, 而使用无符号类型. 相反, 你应该使用断言来保护数据.
    • 如果您的代码涉及容器返回的大小(size),确保其类型足以应付容器各种可能的用法。拿不准时,类型越大越好。
    • 小心整型类型转换和整型提升

64 位下的可移植性

代码应该对 64 位和 32 位系统友好. 处理打印, 比较, 结构体对齐时应切记:

预处理宏

使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.

如果你要宏, 尽可能遵守:

  • 不要在 .h 文件中定义宏.
  • 在马上要使用时才进行 #define, 使用后要立即 #undef.
  • 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
  • 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.
  • 不要用 ## 处理函数,类和变量的名字。

尽可能用 sizeof(varname) 代替 sizeof(type)

使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新

// 推荐
Struct data;
Struct data; memset(&data, 0, sizeof(data));
// 警告
memset(&data, 0, sizeof(Struct));
  • 1
  • 2
  • 3
  • 4
  • 5

auto

  • 用 auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方
  • 程序员必须会区分 auto 和 const auto& 的不同之处,否则会复制错东西
  • auto 还可以和 C++11 特性「尾置返回类型(trailing return type)」一起用,不过后者只能用在 lambda 表达式里。
  • 永远别列表初始化 auto 变量

auto 和 C++11 列表初始化的合体令人摸不着头脑:

auto x(3);  // 圆括号。
auto y{3};  // 大括号。
  • 1
  • 2

它们不是同一回事:

  • x 是 int
  • y 则是 std::initializer_list< int>.

Lambda 表达式

  • 适当使用 lambda 表达式。
    • 匿名函数始终要简短,如果函数体超过了五行,那么还不如起名(即把 lambda 表达式赋值给对象),或改用函数。
  • 别用默认 lambda 捕获,所有捕获都要显式写出来。
-----------不推荐---------
[=](int x) {return x + n;}

--------推荐----------
[n](int x) {return x + n;}
  • 1
  • 2
  • 3
  • 4
  • 5

不要使用复杂的模板编程

只使用 Boost 中被认可的库.

命名约定

通用命名规则

  • 函数命名, 变量命名, 文件命名要有描述性,别心疼空间
  • 少用缩写,除非一些特定的广为人知的缩写
  • 模板参数的命名应当遵循对应的分类:
    • 类型模板参数应当遵循 类型命名 的规则,
    • 非类型模板应当遵循 变量命名 的规则.

文件命名

  • 文件名要全部小写, 可以包含下划线 ( _ ) 或连字符 ( - ), 依照项目的约定. 如果没有约定, 那么 “_” 更好
  • C++ 文件要以 .cc 结尾, 头文件以 .h 结尾. 专门插入文本的文件则以 .inc 结尾
  • 不要使用已经存在于 /usr/include 下的文件名 (即编译器搜索系统头文件的路径), 如 db.h.
  • 通常应尽量让文件名更加明确. http_server_logs.h 就比 logs.h 要好.
  • 定义类时文件名一般成对出现, 如 foo_bar.h 和 foo_bar.cc, 对应于类 FooBar.

类型命名

所有类型命名 —— 类, 结构体, 类型定义 (typedef), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线. 例如:

// 类和结构体
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// using 别名
using PropertiesMap = hash_map<UrlTableProperties *, string>;

// 枚举
enum UrlTableErrors { ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

变量命名

变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用

  • 普通变量命名
string table_name;  // 好 - 用下划线.
string tablename;   // 好 - 全小写.

string tableName;  // 差 - 混合大小写
  • 1
  • 2
  • 3
  • 4
  • 类数据成员
class TableInfo {
  ...
 private:
  string table_name_;  // 好 - 后加下划线.
  string tablename_;   // 好.
  static Pool<TableInfo>* pool_;  // 好.
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 结构体变量
struct UrlTableProperties {
  string name;
  int num_entries;
  static Pool<UrlTableProperties>* pool;
};
  • 1
  • 2
  • 3
  • 4
  • 5

常量命名

声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. 例如:

const int kDaysInAWeek = 7;
  • 1

函数命名

一般来说, 函数名的每个单词首字母大写 (即 “驼峰变量名” 或 “帕斯卡变量名”), 没有下划线. 对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 StartRpc() 而非 StartRPC()).

AddTableEntry()
DeleteUrl()
OpenFileOrDie()
  • 1
  • 2
  • 3

取值和设值函数的命名与变量一致. 一般来说它们的名称与实际的成员变量对应, 但并不强制要求. 例如 int count() 与 void set_count(int count).

命名空间命名

命名空间以小写字母命名.

  • 顶级命名空间的名称应当是项目名或者是该命名空间中的代码所属的团队的名字
  • 命名空间中的代码, 应当存放于和命名空间的名字匹配的文件夹或其子文件夹中.
  • 不要使用缩写作为命名空间
  • 要避免嵌套的命名空间与常见的顶级命名空间发生名称冲突
  • 对于 internal 命名空间, 要当心加入到同一 internal 命名空间的代码之间发生冲突 (由于内部维护人员通常来自同一团队, 因此常有可能导致冲突). 在这种情况下, 请使用文件名以使得内部名称独一无二 (例如对于 frobber.h, 使用 websearch::index::frobber_internal).

枚举命名

枚举的命名应当和 常量 或 宏 一致: kEnumName 或是 ENUM_NAME.

enum UrlTableErrors {
    kOK = 0,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
    OK = 0,
    OUT_OF_MEMORY = 1,
    MALFORMED_INPUT = 2,
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

新代码应该尽可能优先使用常量风格. 但是老代码没必要切换到常量风格, 除非宏风格确实会产生编译期问题.

宏命名

通常 不应该 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线:

#define ROUND(x) ...
#define PI_ROUNDED 3.0
  • 1
  • 2

命名规则的特例

如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略.

  • bigopen(): 函数名, 参照 open() 的形式
  • uint: typedef
  • bigpos: struct 或 class, 参照 pos 的形式
  • sparse_hash_map: STL 型实体; 参照 STL 命名约定
  • LONGLONG_MAX: 常量, 如同 INT_MAX

注释

注释固然很重要, 但最好的代码应当本身就是文档. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字.

  • 要在如何注释及注释风格上确保统一.
  • 不要描述显而易见的现象, 永远不要 用自然语言翻译代码作为注释。你所提供的注释应当解释代码 为什么 要这么做和代码的目的, 或者最好是让代码自文档化
  • 对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释.有时候, 注释确实是为了标记一些未完成的或完成的不尽如人意的地方, 这样一搜索, 就知道还有哪些活要干, 日志都省了
// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
// TODO(bug 12345): remove the "Last visitors" feature
  • 1
  • 2
  • 3
  • 通过弃用注释(DEPRECATED comments)以标记某接口点已弃用.
  • 注释要言简意赅, 不要拖沓冗余, 复杂的东西简单化和简单的东西复杂化都是要被鄙视的;
  • 对于 Chinese coders 来说, 用英文注释还是用中文注释, it is a problem, 但不管怎样, 注释是为了让别人看懂, 难道是为了炫耀编程语言之外的你的母语或外语水平吗;
  • 注释不要太乱, 适当的缩进才会让人乐意看. 但也没有必要规定注释从第几列开始 (我自己写代码的时候总喜欢这样), UNIX/LINUX 下还可以约定是使用 tab 还是 space

格式

  • 每一行代码字符数不超过 80.
    • 包含长路径的 #include 语句可以超出80列.
    • 头文件保护 可以无视该原则.
  • 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.
    • 尽量不使用非 ASCII 字符, 如果使用的话, 参考 UTF-8 格式 (尤其是 UNIX/Linux 下, Windows 下可以考虑宽字符),
    • 尽量不将字符串常量耦合到代码中, 比如独立出资源文件, 这不仅仅是风格问题了;
  • 只使用空格而不是制表符, 每次缩进 2 个空格.
    • NIX/Linux 下无条件使用空格,
    • MSVC 的话使用 Tab 也无可厚非;
  • Lambda 表达式,如果使用引用捕获, 在变量名和 & 之间不留空格.
int x = 0;
auto add_to_x = [&x](int n) { x += n; };
  • 1
  • 2
  • 函数参数, 逻辑条件, 初始化列表: 要么所有参数和函数名放在同一行, 要么所有参数并排分行;
  • 除函数定义的左大括号可以置于行首外, 包括函数/类/结构体/枚举声明, 各种语句的左大括号置于行尾, 所有右大括号独立成行;

函数参数

  • 返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
  DoSomething();
  ...
}

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) {
  DoSomething();
  ...
}

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
    Type par_name1,  // 4 space indent
    Type par_name2,
    Type par_name3) {
  DoSomething();  // 2 space indent
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 未被使用的参数如果其用途不明显的话, 在函数定义处将参数名注释起来:
class Shape {
 public:
  virtual void Rotate(double radians) = 0;
};

class Circle : public Shape {
 public:
  void Rotate(double radians) override;
};

void Circle::Rotate(double /*radians*/) {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 属性, 和展开为属性的宏, 写在函数声明或定义的最前面, 即返回类型之前:
MUST_USE_RESULT bool IsOK();
  • 1

函数调用

函数调用遵循如下形式:

bool retval = DoSomething(argument1, argument2, argument3);
  • 1

如果同一行放不下, 可断为多行, 后面每一行都和第一个实参对齐, 左圆括号后和右圆括号前不要留空格:

bool retval = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);
  • 1
  • 2

参数也可以放在次行, 缩进四格:

if (...) {
  ...
  ...
  if (...) {
    DoSomething(
        argument1, argument2,  // 4 空格缩进
        argument3, argument4);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果一些参数本身就是略复杂的表达式, 且降低了可读性, 那么可以直接创建临时变量描述该表达式, 并传递给函数:

int my_heuristic = scores[x] * y + bases[x];
bool retval = DoSomething(my_heuristic, x, y, z);
  • 1
  • 2

或者放着不管, 补充上注释:

bool retval = DoSomething(scores[x] * y + bases[x],  // Score heuristic.
                          x, y, z);
  • 1
  • 2

此外, 如果一系列参数本身就有一定的结构, 可以酌情地按其结构来决定参数格式:

// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
                    y1, y2, y3,
                    z1, z2, z3);
  • 1
  • 2
  • 3
  • 4

列表初始化格式

// 一行列表初始化示范.
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}};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

条件语句

倾向于不在圆括号内使用空格.

if (condition) {  // 圆括号里没有空格.
  ...  // 2 空格缩进.
} else if (...) {  // else 与 if 的右括号同一行.
  ...
} else {
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果能增强可读性, 简短的条件语句允许写在同一行. 只有当语句简单并且没有使用 else 子句时使用:

if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();
  • 1
  • 2

Google 强调有一对 if-else 时, 不论有没有嵌套, 都要有大括号

// 只要其中一个分支用了大括号, 两个分支都要用上大括号.
if (condition) {
  foo;
} else {
  bar;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

循环和开关选择语句

switch 语句中的 case 块可以使用大括号也可以不用, 取决于你的个人喜好. 如果用的话, 要按照下文所述的方法.

如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning). 如果 default 应该永远执行不到, 简单的加条 assert:

switch (var) {
  case 0: {  // 2 空格缩进
    ...      // 4 空格缩进
    break;
  }
  case 1: {
    ...
    break;
  }
  default: {
    assert(false);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

空循环体应使用 {} 或 continue, 而不是一个简单的分号.

while (condition) {
  // 反复循环直到条件失效.
}
for (int i = 0; i < kSomeNumber; ++i) {}  // 可 - 空循环体.
while (condition) continue;  // 可 - contunue 表明没有逻辑.


while (condition);  // 差 - 看起来仅仅只是 while/loop 的部分之一.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

指针和引用表达式

  • 在访问成员时, 句点或箭头前后没有空格.
  • 指针操作符 * 或 & 后没有空格.
x = *p;
p = &x;
x = r.y;
x = r->y;
  • 1
  • 2
  • 3
  • 4
  • 在声明指针变量或参数时, 星号与类型或变量名紧挨都可以:
// 好, 空格前置.
char *c;
const string &str;

// 好, 空格后置.
char* c;
const string& str;


int x, *y;  // 不允许 - 在多重声明中不能使用 & 或 *
char * c;  // 差 - * 两边都有空格
const string & str;  // 差 - & 两边都有空格.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在单个文件内要保持风格一致, 所以, 如果是修改现有文件, 要遵照该文件的风格.

布尔表达式

如果一个布尔表达式超过 标准行宽, 断行方式要统一一下.

下例中, 逻辑与 (&&) 操作符总位于行尾:

if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another && last_one) {
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

注意, 上例的逻辑与 (&&) 操作符均位于行尾. 这个格式在 Google 里很常见, 虽然把所有操作符放在开头也可以. 可以考虑额外插入圆括号, 合理使用的话对增强可读性是很有帮助的. 此外, 直接用符号形式的操作符, 比如 && 和 ~, 不要用词语形式的 and 和 compl

函数返回值

  • 不要在 return 表达式里加上非必须的圆括号.
  • 只有在写 x = expr 要加上括号的时候才在 return expr; 里使用括号.
//-----------------------
return result;                  // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
        another_condition);

//-----------------
return (value);                // 毕竟您从来不会写 var = (value);
return(result);                // return 可不是函数!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

预处理指令不要缩进, 从行首开始.

即使预处理指令位于缩进代码块中, 指令也应从行首开始.

// 好 - 指令从行首开始
  if (lopsided_score) {
#if DISASTER_PENDING      // 正确 - 从行首开始
    DropEverything();
# if NOTIFY               // 非必要 - # 后跟空格
    NotifyClient();
# endif
#endif
    BackToNormal();
  }



// 差 - 指令缩进
  if (lopsided_score) {
    #if DISASTER_PENDING  // 差 - "#if" 应该放在行开头
    DropEverything();
    #endif                // 差 - "#endif" 不要缩进
    BackToNormal();
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

类格式

访问控制块的声明依次序是 public:, protected:, private:, 每个都缩进 1 个空格.

class MyClass : public OtherClass {
 public:      // 注意有一个空格的缩进
  MyClass();  // 标准的两空格缩进
  explicit MyClass(int var);
  ~MyClass() {}

  void SomeFunction();
  void SomeFunctionThatDoesNothing() {
  }

  void set_some_var(int var) { some_var_ = var; }
  int some_var() const { return some_var_; }

 private:
  bool SomeInternalFunction();

  int some_var_;
  int some_other_var_;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

构造函数初始值列表

// 如果所有变量能放在同一行:
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) {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

命名空间内容不缩进.

命名空间 不要增加额外的缩进层次, 例如:

namespace {

void foo() {  // 正确. 命名空间内没有额外的缩进.
  ...
}

}  // namespace

//------------------------
namespace {

  // 错, 缩进多余了.
  void foo() {
    ...
  }

}  // namespace

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

声明嵌套命名空间时, 每个命名空间都独立成行.

namespace foo {
namespace bar {
  • 1
  • 2

水平留白

通用

水平留白的使用根据在代码中的位置决定. 永远不要在行尾添加没意义的留白.

void f(bool b) {  // 左大括号前总是有空格.
  ...
int i = 0;  // 分号前不加空格.
// 列表初始化中大括号内的空格是可选的.
// 如果加了空格, 那么两边都要加上.
int x[] = { 0 };
int x[] = {0};

// 继承与初始化列表中的冒号前后恒有空格.
class Foo : public Bar {
 public:
  // 对于单行函数的实现, 在大括号内加上空格
  // 然后是函数实现
  Foo(int b) : Bar(), baz_(b) {}  // 大括号里面是空的话, 不加空格.
  void Reset() { baz_ = 0; }  // 用空格把大括号与实现分开.
  ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

循环和条件语句

if (b) {          // if 条件语句和循环语句关键字后均有空格.
} else {          // else 前后有空格.
}
while (test) {}   // 圆括号内部不紧邻空格.
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) {    // 循环和条件语句的圆括号里可以与空格紧邻.
if ( test ) {     // 圆括号, 但这很少见. 总之要一致.
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) {  // 循环里内 ; 后恒有空格, ;  前可以加个空格.
switch (i) {
  case 1:         // switch case 的冒号前无空格.
    ...
  case 2: break;  // 如果冒号有代码, 加个空格.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

操作符

// 赋值运算符前后总是有空格.
x = 0;

// 其它二元操作符也前后恒有空格, 不过对于表达式的子式可以不加空格.
// 圆括号内部没有紧邻空格.
v = w * x + y / z;
v = w*x + y/z;
v = w * (x + z);

// 在参数和一元操作符之间不加空格.
x = -5;
++x;
if (x && !y)
  ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

模板和转换

// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有.
vector<string> x;
y = static_cast<char*>(x);

// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector<char *> x;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

垂直留白越少越好.

这不仅仅是规则而是原则问题了: 不在万不得已, 不要使用空行. 尤其是: 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/386541
推荐阅读
相关标签
  

闽ICP备14008679号