参考链接:Google C++ Style Guide

# 头文件

  • 每个 .cpp 源文件都应该有一个相对应的 .h 头文件
  • 每个头文件都应该有头文件守卫,守卫格式统一为: _<PROJECT>_<PATH>_<FILE>_H_
  • 对于头文件中对于 inline 内联函数的定义,要求函数体在十行以内,且不包括循环、switch 语句。
  • 头文件的 include 顺序:相关头文件、C 系统头文件、C++ 头文件、其他库头文件、项目头文件
    (这里的 “相关头文件” 指的是与该源文件同名的头文件)

# 作用域

  • 除了少数例外,将代码放置在命名空间中,以减少命名冲突

  • 不要使用 using namespace xxx 来导入命名空间中的内容(会污染命名空间)

    1
    2
    // Forbidden -- This pollutes the namespace.
    using namespace foo;

  • 当源文件中的定义不需要被其他文件所引用时,通过未命名命名空间或 static 来赋予其内部链接性

    1
    2
    3
    namespace {
    ...
    } // namespace

  • 用注释来结束命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // In the .h file
    namespace mynamespace {

    // All declarations are within the namespace scope.
    // Notice the lack of indentation.
    class MyClass {
    public:
    ...
    void Foo();
    };

    } // namespace mynamespace

# 对于局部变量
  • 将其放在尽可能小的范围内,并在声明中初始化变量。

  • 定义局部变量时尽可能地靠近其第一次被使用的地方,便于读者找到定义并了解其初始值

    1
    2
    3
    4
    5
    6
    7
    int jobs = NumJobs();
    // More code...
    f(jobs); // Bad -- declaration separate from use.


    int jobs = NumJobs();
    f(jobs); // Good -- declaration immediately (or closely) followed by use.

  • 局部变量应该在定义时初始化,而不是先定义后初始化:

    1
    2
    3
    4
    5
    int i;
    i = f(); // Bad -- initialization separate from declaration.


    int i = f(); // Good -- declaration has initialization.

1
2
3
4
5
6
std::vector<int> v;
v.push_back(1); // Prefer initializing using brace initialization.
v.push_back(2);


std::vector<int> v = {1, 2}; // Good -- v starts initialized.

#

  • 避免在类的构造函数中调用虚函数,详见虚函数与构造函数
  • 避免隐式转换,尽可能地使用 explicit 关键字来修饰单一参数的函数(拷贝构造和移动构造除外)
  • 只对携带数据的被动对象( passive objects )使用 struct ,其他一律使用 class
  • 尽可能地考虑组合,而不是继承。当使用继承时,优先公有继承
  • 对于纯抽象类,其派生类的继承属于 “接口继承”,其他情况则都是 “实现继承”。
  • 强烈不建议多实现继承
  • 类的成员变量应该是私有的

其中,"passive objects" 指的是那些仅仅用于存储数据而不包含任何行为的对象。换句话说,它们是纯粹的数据结构,用于在程序中传递数据,而不负责执行任何操作或行为。通常,这些对象仅包含公共数据成员和对这些数据成员进行初始化、访问和设置的方法,而不包含任何其他的逻辑或操作。

这种对象通常用于数据传递和数据存储,而不涉及到具体的行为。它们被设计为尽可能简单和轻量,只提供了对数据的简单操作,例如读取和写入。因此,当文本中提到 "passive objects" 时,意思是将结构体(struct)仅用于这种数据容器的情况。

另外,在 C++ 中,“多实现继承” 指的是一个类同时从多个基类继承属性和方法,可能会导致 菱形继承 问题。

# 函数

  • 对于函数的输出,优先使用返回值进行传递,而不是使用输出参数
  • 对于函数的返回值,优先返回值(return by value),而不是返回引用(return by reference)
  • 避免返回裸指针,除非它可以为 nullptr
  • 对于函数的输入参数,应该采用值传递或者 const 的引用传递(pass by value, or pass by reference of const)
  • 对于函数的输出参数和输入输出参数,应该采用引用传递
  • 对于函数参数的顺序,优先输入参数,然后是输出参数
  • 函数体尽可能短小(40 行左右),一个函数就应该集中做某件事情

一个函数应该避免依赖某个引用参数在整个函数调用期间的存活(Avoid defining functions that require a reference parameter to outlive the call.)即,某个引用参数的存活与否,不应该影响该函数的正常运作,其不会因为某个引用参数在函数调用过程中不再存活而出错。

对于函数的返回值,存在两种返回语法,一种是普通的、常见的,返回类型出现在函数名之前;而另一种则是在函数名之前用 auto 进行占位,而真正的返回值则尾随函数的参数列表(这种语法我们称之为 Trailing Return Type Syntax ):

1
auto foo(int x) -> int;

对于 Trailing Return Type Syntax ,其于普通的返回形式不同之处在于,这种情况下返回类型存在于函数体的作用域中,则对于以下情况能够有较好的表现:

1
2
3
4
5
6
7
//普通的“前置”返回
template <typename T, typename U>
decltype(declval<T&>() + declval<U&>()) add(T t, U u);

//“后置”返回
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u);

总得来说,在大多数情况下,我们优先考虑普通的 “前置” 返回,而当我们在处理涉及 lambda 表达式时,或者当将函数的返回类型后置能够带来更好的阅读体验时,我们采用 “后置” 的 Trailing Return Type Syntax

# 所有权与智能指针

  • 首选使用 std::unique_ptr 来明确所有权转移
  • 转移所有权比 “借用” 指针或引用更方便,它减少了用户之间对于内存对象的生存期的协同。
    (所有权明确了谁改对内存的生存期负责,明确了谁该最终释放内存,而单纯地指针拷贝、借用则会混淆这一点)

如果动态分配是必要的,最好将所有权闲置 / 保留在分配它的代码中。此后,如果其他代码需要访问对象,考虑向其传递一个副本,或者传递一个指针或引用而不转移所有权。(我猜这样做的目的是,将所有权保留在原先分配它的内存的位置,让原 own 负责维护其生存期,进而避免所有权的混乱。)

# C++ 特性

# 类型转换
  • 使用 C++ 风格的类型转换( static_castconst_castreinterpret_cast )而不是 C 风格的类型转换
  • 不要使用 C 风格的转换(例如 (int)x ) 除非转换的目标类型是 void
  • 当且仅当 T 是一个类类型时使用 T(x) 这种转换形式
  • 使用大括号初始化来转换算术类型(例如 int64_t{x}
  • static_cast 进行类类型的向上、向下转换
  • const_cast 来移除 const 限定词
  • reinterpret_cast 来进行不安全的类型转换,当且仅当你知道你在干什么的时候使用
# 其他
  • 优先使用前置形式的自增、自减运算符 ++-- ,(即 ++i--i )除非你需要用到后置自增 / 自减的结果。
  • 优先使用 using 语法而不是 typedef 语法进行别名
  • 对于空指针,使用 nullptr ,对于空字符(串),使用 \0
  • 优先考虑使用 sizeof (变量名) 而不是 sizeof (类型名)
    原因是 sizeof (变量名) 会随着类型的改变而进行适当的更新
    1
    2
    3
    4
    MyStruct data;
    memset(&data, 0, sizeof(data)); //good

    memset(&data, 0, sizeof(MyStruct)); //bad
# const 限定
  • 在 API 中,尽可能地(合理)使用 const,用于修饰类函数、函数传参
  • 对于以值传递的函数参数,加 const 修饰无意义,因为它不会影响到调用者传入的数据
  • 对于 const 修饰符,使用前置的书写形式,即 const int* foo
# 无符号整型
  • 尽可能地不要使用无符号整型(unsigned integer)诸如 uint32_t ,除非有一个合理有效的理由:用于表示位图
  • 不要因为 “一个数字不能出现负值” 这样的原因而使用无符号整型
# 宏定义
  • 避免定义宏,禁止使用宏定义 C++ API
  • 对于常量,用 const 变量代替,对于缩写,用引用代替,总之,尽可能避免使用宏定义
# 类型推导
  • 当且仅当类型推导使得代码变得更清晰和安全的情况下,使用类型推导
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    std::unique_ptr<WidgetWithBellsAndWhistles> widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
    absl::flat_hash_map<std::string, std::unique_ptr<WidgetWithBellsAndWhistles>>::const_iterator
    it = my_map_.find(key);
    std::array<int, 6> numbers = {4, 8, 15, 16, 23, 42};


    //it's ok and better below:
    auto widget = std::make_unique<WidgetWithBellsAndWhistles>(arg1, arg2);
    auto it = my_map_.find(key);
    std::array numbers = {4, 8, 15, 16, 23, 42};

# 命名

  • 减少使用缩写,命名的描述性(详细程度)应该与该命名在作用域的可见性成正比
    (例如,在一个只有 5 行的代码中,变量名 n 可以被接受,但如果是在一个大段代码的类中,则 n 有点显得过于模糊了)

一些 “常见的” 缩写可以被接受,比如用 i 来表示迭代(iteration)次数,用 T 来表示模板参数(template parameter)

# 文件名的命名规范

全小写,单词之间以下划线( _ )或破折号( - )进行连接,优先考虑下划线( _

1
2
my_useful_class.cpp
my_useful_class.h

# 类型名的命名规范

以大写字母开头,每个单词首字母大写,不带任何下划线:

1
2
3
4
5
6
7
8
9
10
11
12
13
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map<UrlTableProperties *, std::string> PropertiesMap;

// using aliases
using PropertiesMap = hash_map<UrlTableProperties *, std::string>;

// enums
enum class UrlTableError { ...

这里我们所说的类型,包括了 classes、structs、type aliases、enums 和 type template parameters

# 变量名的命名规范

采用蛇形命名法,全小写,每个单词之间用下划线( _ )进行连接
特殊地,对于类内的类成员变量,在变量名的前面加一个前缀的下划线( _
(在谷歌命名规范中,它们采用后缀下划线,但我 prefer 前缀下划线)

1
2
3
4
5
6
7
8
std::string table_name;

class TableInfo {
...
private:
std::string _table_name;
static Pool<TableInfo>* _pool;
};

# 常量名的命名规范

对于用 const 和 constexpr 描述的变量,或其值在程序的整个生存期保持固定不变的变量,采用 “全大写 + 蛇形命名” 的形式:
(在谷歌规范中,用一个小写的前缀 “k” 开头,然后其余每个单词首字母大写。个人不赞同不喜欢这种风格):

1
2
3
4
5
constexpr uint32_t MAX_FRAME_WIDTH = 640;
constexpr uint32_t MAX_FRAME_HEIGHT = 480;
constexpr uint16_t MAX_PHASE = 4095;
constexpr int RGB_FRAME_WIDTH = 1920;
constexpr int RGB_FRAME_HEIGHT = 1080;

谷歌风格常量:

1
2
3
const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24; // Android 8.0.0
//特殊地,对于无法用过首字母大写进行分隔的情况,允许使用下划线,如上面的数字 8.0.0 的情况

# 枚举类型的命名规范

枚举类型本质上也是一种常量,所以其命名规则同常量名,小写字母 “k”+ 每个单词首字母大写。

# 命名空间的命名规范

在谷歌规范中,其对命名空间的约定为:全小写,单词以下划线分隔。
但我不喜欢这种风格,更偏向于同类型名,以大写字母开头,每个单词首字母大写,不带任何下划线

# 注释

  • 好的代码自我阐释。

  • 函数定义时最好在函数前添加注释,用于形容函数的作用以及用法

  • 对于函数的参数,如果是指针类型,可以备注其是否允许为空指针,以及如果是会怎样

  • 对于类内成员,可以添加注释形容某个变量的默认值 / 初始化值的取值原因 or 含义:

    1
    2
    3
    4
    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_;

  • 对于 TODO 类型的注释,其用于描述临时解决方案、短期解决方案、足够好但不够完美的方案。

  • TODO 类型的注释以全大写的 “TODO” 开头,其后跟随 bug ID、姓名、邮箱地址以及问题描述。

  • TODO 最好包含一个 “什么时候做某事” 的截止日期或具体的事件(时机)

# 格式

# 基本规范
  • 对于每行代码的长度,尽量不超过 80 个字符(不要有单行过长的代码)
  • 尽可能避免使用非 ASCII 的字符(包括但不限于中文字符(中文注释))
  • 采用 4 个空格作为缩进,不使用 tabs,或者在编辑器中将 tab 设置为输出 4 个空格
    (原文采用的是 2 个空格作为缩进,但我不喜欢)
  • 每行代码行末不应该尾随多余的空格,对于行末的注释,间隔两个空格之后开始,注释内容与 // 之间留一个空格
    1
    int i = 0;  // Two spaces before end-of-line comments.
# 对于函数定义
  • 返回值与函数名处于同一行,函数参数尽可能也与函数名处于同一行。
  • 左花括号处于一行的行末,与前面的内容之间存在一个空格。
  • 如果参数过多,则可适当分行,第一个参数保留在与函数名同一行的位置,后续参数则与第一个参数对齐。
    1
    2
    3
    4
    ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
    DoSomething();
    ...
    }

由于这种换行方式,为了避免换行的函数参数与函数体内容混淆,则在这种情况下,把花括号另起一行:

1
2
3
4
5
6
7
ReturnType ClassName::ReallyLongFunctionName(Type par_name1,
Type par_name2,
Type par_name3)
{
DoSomething();
...
}

# 对于函数调用

与函数定义的情况基本一致,如果函数传参过多,则换行,第一个参数与函数名同一行,其余参数换行后与第一个参数对齐:

1
2
bool result = DoSomething(averyveryveryverylongargument1,
argument2, argument3, argument4);

# 对于分支与循环语句
  • if、while 和 for 之后紧跟左圆括号,不留空格
  • else、else if 前后各留一个空格
    (这里与原文的 preference 略有偏差,我 prefer 在 if、while、for 和条件之间不希望有空格)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    if(condition) {
    DoOneThing();
    DoAnotherThing();
    } else if (int a = f(); a != 3) { //分号之后要有空格隔开
    DoAThirdThing(a);
    } else {
    DoNothing();
    }

    // Good - the same rules apply to loops.
    while(condition) {
    RepeatAThing();
    }

    // Good - the same rules apply to loops.
    do {
    RepeatAThing();
    } while (condition);

    // Good - the same rules apply to loops.
    for(int i = 0; i < 10; ++i) { //分号之后要有空格隔开
    RepeatAThing();
    }

以下情况不被鼓励:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(condition)
foo;
else {
bar;
}


if(x) DoThis();
else DoThat();


if(condition)
DoSomething();


if(x == kFoo) { return new Foo(); } //仅当我们定义 getter 或 setter 函数时鼓励这种写法

# 对于空循环

1
2
3
4
5
while(condition) {}  // Good - `{}` indicates no logic.
while(condition) {
// Comments are okay, too
}
while(condition) continue; // Good - `continue` indicates no logic.

以下情况不被鼓励:

1
while(condition);  // Bad - looks like part of `do-while` loop.

# 对于指针和引用

采用 “尾随空格” 的形式,即变量类型紧跟星号 * 或引用符 & ,之前不含空格,其后再与变量名之间填充一个空格:
(特殊地,在 <> 表示的模板类或类型转换中,指针或引用不需要任何空格)

1
2
3
4
5
char* c;
const std::string& str;
int* GetPointer();
std::vector<char*> // Note no space between '*' and '>'
y = static_cast<char*>(x);

另外,当定义指针和引用时,为了避免混淆和误导,不允许在单行定义多个变量:

1
2
3
//以下情况被强烈禁止
int x, *y; // Disallowed - no & or * in multiple declaration
int* x, *y; // Disallowed - no & or * in multiple declaration; inconsistent spacing

# 对于条件

对于条件语句和循环语句中的条件表达式,如果存在多个条件,则可适当进行换行,每行以条件的逻辑运算符(如 &&|| )结尾,(大致上与函数定义的逻辑一致):

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

# 对于预处理指令

预处理指令顶格书写,不允许缩进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Good - directives at beginning of line
if (lopsided_score) {
#if DISASTER_PENDING // Correct -- Starts at beginning of line
DropEverything();
NotifyClient();
#endif
BackToNormal();
}


// Bad - indented directives
if (lopsided_score) {
#if DISASTER_PENDING // Wrong! The "#if" should be at beginning of line
DropEverything();
#endif // Wrong! Do not indent "#endif"
BackToNormal();
}

# 对于类内的格式

采用 public、protected、private 的顺序进行代码书写,这三个关键字不缩进(谷歌规范则缩进一个空格,我不喜欢)

# 对于命名空间

其内容不应该 / 不需要因此而缩进一级:

1
2
3
4
5
6
7
namespace {

void foo() { // Correct. No extra indentation within namespace.
...
}

} // namespace