跳转到内容

C++代码风格

来自ACM Class Wiki

本文档是 Google C++ 代码规范 的节选。

在 code review 过程中,助教会对代码风格进行检查。你不一定要使用 Google 代码风格,但是请务必保持代码的一致性。文档中也会用斜体标注 Google 之外的代码风格要求。

文档中的格式要求大多在 IDE 中都有对应的设定。

单行长度

每一行的长度不应比 80 个字符长。(你未必一定需要将单行长度控制在 80 个字符内,但是请避免某行特别长的情况,特别长的代码行会大大降低可读性。)

例外:

  • 无法分割的注释,如长链接;
// 代码风格参考 https://acm.sjtu.edu.cn/wiki/Programming_2022/%E4%BB%A3%E7%A0%81%E9%A3%8E%E6%A0%BC
  • 无法分割的字符串字面量(分割会使得内容更难被理解);
const char* long_string = "this is a really really really really really really really really long string"; 
  • include 语句;
#include <a_really_really_really_really_really_really_really_really_really_really_long_header_file.h>
  • 头文件保护;
#ifdef PROJECT_FOO_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_LONG_HEADER_FILE_H_
#define PROJECT_FOO_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_REALLY_LONG_HEADER_FILE_H_

...

#endif
  • using 语句。
using really_really_really_really_long_namespace::really_really_really_really_long_class_name;

留白与缩进

纵向留白

  • 不应有超过两个连续的空行;(如果你的代码风格将违背此部分中的内容,请事先联系助教!
  • 如需将括号中内容分成多行,请将括号放置在括号内容的最后一行末尾,而不是新开一行。(你未必一定要遵守此要求,但请保持统一。
void Foo() {
  if (...) {
    ...
  } else {
    ...
  }
}
void ReallyReallyReallyReallyReallyLongFoo(
    int input1, int input2,
    int input3, int input4) {
  ...
}

横向留白

使用空格 (space) 而非制表符 (tab) 作为缩进符号

如果你的代码风格将违背此部分中的内容,请事先联系助教!

运算符

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 一元运算符与操作数之间不加空格;
x = -a;
  • .-> 两侧不加空格;
classA.memberA = x; // . 两侧不加空格
classPtr->memberB = y; // -> 两侧不加空格
  • 多元(含二元、三元)运算符前后加一个空格;
x=a+b/c*d+f*a; // 错误
x = a + b / c * d + f * a; // 正确
  • 圆括号内无紧邻的空格;
x = ( a + b ) / ( c * d ); // 错误
x = (a + b) / (c * d); // 正确
  • 声明指针或引用变量时, *& 周围的空格可在任意一侧,但需要保持一致。
int* some_variable_ptr;
int *some_variable_ptr;
int * some_variable_ptr; // 不允许 (不要两侧都加空格)
int* i, *j; // 不允许 (不要同时声明两个指针类型的变量; 空格位置不统一)

分支、循环语句

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 关键词不是函数名称,需要与后方的括号留有空格;
if (condition) { // 正确
  ...
}
for (int i = 0; i < n; ++i) { // 正确
  ...
}
  • 我们建议所有分支语句均使用花括号;
if (condition1) { // 正确
  ...
} else if (condition2) { // 正确
  ...
} else b = d; // 错误,必须用花括号包裹
  • 空循环体不能直接写一个分号,可用 {}continue; 填充;
while (true) {} // 正确
while (true) {
  continue;
} // 正确
while (true); // 错误
  • 如果你希望将花括号内的语句压到一行(不建议),那么需要在花括号内部加上空格。
while (a > 0) { ++i; } // 正确
while (a > 0) {++i;} // 错误

函数

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 函数调用和函数声明时,函数名称与函数参数的括号之间不加空格;
void Function1(int input) { // 正确
  ...
}
void Function1 (int input) { // 错误
  ...
}
  • 函数参数的小括号 ) 与函数体花括号 { 间加一个空格;
void Function1(int input){ // 错误
  ...
}
  • return 语句不是函数调用。因此,除非非常必要,否则在 return 后通常不加括号;
void Function1(int input) {
  ...
  return x; // 正确
  return (x); // 错误 (不应加括号)
  return(x); // return 与返回值间需要加空格
}
  • 如果花括号内的语句可以压到一行,则需要在花括号内部加一个空格。
int Add(int x, int y) { return x + y; }

缩进

你未必一定要遵守此要求,但请保持统一。

代码样例:

namespace some_namespace {

void Fun1(int input1, // namespace 不带有额外缩进
          int input2,
          int input3) {
  DoSomething();
  if (...) {
    DoSomething();
  }
}
#ifdef SOME_MACRO_DEFINE
void ReallyReallyReallyLongFunction(
    int input1,
    int input2) { ...; } // 4 个空格
#endif // SOME_MACRO_DEFINE

} // namespace some_namespace
  • 在函数内部、嵌套的结构中,保持额外 2 个空格的缩进;
  • 函数的参数对齐;
  • 在函数声明、定义、调用中,如果无法在函数名那一行写下所有参数,则新开一行,并带有 4 个空格缩进;
void ReallyReallyReallyLongFunction(
    int input1,
    int input2) { ...; } // 4 个空格
#endif // SOME_MACRO_DEFINE
  • 无论如何,预编译指令不带有任何缩进;
  • namespace 不带有额外缩进。
  • 关于类和结构体的缩进,参见类和结构体的格式章节

命名

  • 基础原则(如果你的代码风格将违背此部分中的内容,请事先联系助教!
    • 避免缩写(程序设计中熟知的除外)
    • 尽量将变量作用表达完整
    • 避免意思冗余
  • 命名格式(你未必一定要遵守此要求,但请在你的整个项目中不同类型的命名格式统一。)
    • 文件名:下划线 priority_queue.h
      • 头文件: .h (priority_queue.h)
      • 源文件 (source file): .cc (main.cc)
      • 非头文件(通常是自动生成的): .inc (auto_generated.inc)
    • 函数名:大驼峰 SomeFunction
    • 类型名:大驼峰 SomeClass
    • 变量名:下划线 some_variable
      • 对于类中的变量,其格式为下划线命名法后加一个下划线,如 some_variable_
    • 常量名:小驼峰(k 开头) kConstant
    • 枚举量:小驼峰(k 开头) kEnumA
    • 命名空间名:下划线 some_namespace
    • 宏名:全大写,词间以下划线分隔 SOME_MACRO

注释

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 注释可以大大提高代码的可读性;
  • 尽管注释很重要,但是变量、函数等等最好可以从名称就直接能理解其作用;
  • 多行注释的语法可以用 ///* */,但请保持统一(通常建议 //);
  • 如果对象的名称无法体现其作用和用法,则需要写注释详细说明;
  • 如果同时存在声明和定义,则在声明处写注释;
  • 如果代码表面上的含义与其实际作用并不完全相同,需要注释详细说明(特别是函数内部和变量声明),如:
// Used to bounds-check table accesses. -1 means
// that we don't yet know how many entries the table has.
int num_total_entries;
  • 注意标点、拼写和语法,尽量写完整的句子而非零碎的词语。

函数

尽量写短函数

如果你的代码风格将违背此部分中的内容,请事先联系助教!

函数尽可能简短,尽可能把程序分成比较小函数(除非拆分会降低可读性)。

函数简短、功能明确,可以减轻开发、后期维护以及修改的成本,也可以更方便地找到问题。因此,请尽量把长函数细分成更好维护的小函数。

不过,如果细分函数会造成显著的性能问题(经编译器优化后一般不会),或细分后的函数更难以理解,那请保留原来的函数。

inline 函数

如果你的代码风格将违背此部分中的内容,请事先联系助教!

通常编译器会自动决定是否内联,因此无需特地用 inline 关键词。inline 函数仅对当前翻译单元有效。

对于过长的函数,将此函数内联到其他函数反而会降低程序性能,增大可执行文件大小,因此不建议将过长的函数内联。

inline 函数不能多于 10 行。

函数重载 (Function Overloading)

如果你的代码风格将违背此部分中的内容,请事先联系助教!

仅当同名函数的语意完全一致时,才能采用函数重载。

关于是否合适,一个很好的标志是,如果你能对所有的同名函数采用相同的文档注释,那这样的函数重载是非常好的。

函数默认值 (Default Arguments)

如果你的代码风格将违背此部分中的内容,请事先联系助教!

可以在非虚函数中使用函数参数默认值。禁止在虚函数中使用函数参数默认值。

函数缺省参数必须为常值。

采用函数默认值的时机与函数重载相同,仅当同名函数的语意完全一致时,才能采用函数重载。(参见函数重载 (Function Overloading) 章节

如果对是否采用函数默认值有所犹豫,请采用函数重载 (Function Overloading)

类和结构体 (Classes and structs)

使用类还是使用结构体?

如果你的代码风格将违背此部分中的内容,请事先联系助教!

如果只是存简单数据,请使用结构体 (struct);否则使用类 (class)。

类和结构体的格式

你未必一定要遵守此要求,但请保持统一。

代码样例

class MyClass : public OtherClass {
 public:      // 1 个空格的缩进
  MyClass();  // 通常的 2 个空格的缩进
  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_;
};

被继承的基类需要在声明类的当前行。(可以无视一行至多 80 个字符原则)(你未必一定要遵守此要求,但请保持统一。

publicprotectedprivate 关键词

你未必一定要遵守此要求,但请保持统一。

  • public:protected:private: 关键词仅 1 个空格缩进;
  • 对于 public:protected:private: 关键词,除第一个声明的外,剩下的关键词前需要有一行空行(很简短的类除外);
  • public:protected:private: 关键词后无空行;
  • 声明顺序为(如果没有则省略)
    • public:
    • protected:
    • private:

声明顺序

你未必一定要遵守此要求,但请保持统一。

(依据 publicprotectedprivate 关键词章节,)一个类总是先 public:,再 protected:,最后 private:;如没有某一部分,则省略。

各部分中,声明顺序为

  1. 类型和类型别名(typedef, using, enum, 内嵌类或结构体,友元类型)
  2. static 常量
  3. 生成类的工厂函数
  4. 构造函数 (constructor) 和赋值运算符 (operator=)
  5. 析构函数 (destructor)
  6. 其他函数(含静态和非静态成员函数,及友元函数)
  7. 成员变量(静态和非静态成员变量)

命名

你未必一定要遵守此要求,但请保持统一。

  • 函数的命名与普通函数相同;
  • 常量的命名与普通常量相同;

对于变量的命名

  • 类中的变量命名为下划线命名法,并在最后加上一个下划线,如
class TableInfo {
  ...
 private:
  std::string table_name_;  // 末尾有下划线
};
  • 结构体中的命名为下划线命名法,最后无需下划线,如
struct UrlTableProperties {
  std::string name;
};

继承与组合

多数情况下,请采用组合的方式——将需要的对象以成员的方式放在类中;只有当「当前类是一种基类」(如赋值语句是一种语句,则「赋值语句」类可以继承「语句」类)的时候,才可以采用继承。

采用继承时,请使用 public 继承。其他情况下,请使用组合。

允许使用多继承,但请注意每个基类都需要符合以上的要求。

虚函数

对于派生类的虚函数,请使用 overridefinal 关键词,不要使用 virtual 关键词。

类中成员的可见性

(你未必一定要遵守此要求,建议采用以下建议。)

为了防止各类危险的行为(如使用者继承此类),所有数据变量都应当是 private 的。

对于派生类需要调用,但派生类不能调用调用的成员函数,请使用 protected

类或结构体中的隐式转换

(你未必一定要遵守此要求,建议采用以下建议。)

隐式转换会导致函数调用时出现本不匹配的参数,因此尽可能避免隐式转换。

在具体的写代码的过程中,除了复制构造函数和移动构造函数外,其他能以单参数传入的构造函数必须加上 explicit 关键词(std::initializer_list 这类的参数也可以省略 explicit 关键词)。

其他

变量

  • 不要在一个语句中声明多个指针或引用类型的变量;(如果你的代码风格将违背此部分中的内容,请事先联系助教!
  • 尽可能在变量声明时赋值。(你未必一定要遵守此要求,但建议尽可能在变量声明时赋值。)

类型转换

(你未必一定要遵守此要求,建议采用以下建议。)

为避免出现二义性,建议使用 C++ 的转换,不建议使用 C 的转换。

除此以外,不建议使用 dynamic_cast(不规范的运行时类型推断会导致代码很难维护),通常的设计不需要使用运行时类型推断来区分类,如果实在有需求,建议通过设置成员函数(建议返回值为 enum)来区分,并视需求采用 static_cast

空指针

如果你的代码风格将违背此部分中的内容,请事先联系助教!

对于指针,请使用 nullptr;对于字符串,请使用 '\0'

不要使用 NULL0,这会导致出现隐式转换。

对于下面的代码:

void Foo(int i) {
  std::cout << "int" << std::endl;
}

void Foo(int* i) {
  std::cout << "int*" << std::endl;
}

int main() {
  Foo(NULL); // int
  Foo(0); // int
  Foo(nullptr); // int*

以上代码将会输出

int
int
int*

sizeof

(你未必一定要遵守此要求,建议采用以下建议。)

为方便代码维护,推荐采用 sizeof(varname) 而非 sizeof(type)

特别地,如果没有对应的变量时(或当前语境与变量无关时),请采用 sizeof(type)

运算符重载 (Operator Overloading)

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 当且仅当运算符意思完全符合当前语境时,采用运算符重载;
  • 尽可能地贴合语境,比如当定义了 < 时,最好也要定义 >
  • 尽量定义非成员函数的运算符重载,避免出现 a < b 能编译,但 b < a 不能编译;
  • 不要完全不采用运算符重载,===<<Equals()CopyFrom()PrintTo() 更易读;
  • 不要重载 &&||,(逗号),或一元 &.
  • 不要使用用户定义的字面量。

可见性

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 关于类中成员的可见性,参见类中成员的可见性章节
  • 如果你需要让某个编译单元(.cc 文件)的某个函数不可见,可以放到无名 namespace 中或标记为 static。
namespace {
void PrivateFun1() {
  ...
}
}  // namespace
static void PrivateFun1() {
  ...
}

宏 (Macros)

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 尽可能避免一切的宏定义;
  • 代表常量的宏尽量用 constexpr 修饰的常量代替;
#define PI (3.14)

替换为

constexpr int kPI = 3.14;
  • 带参数的宏尽量使用模板 (templates) 来代替。
#define max(a, b) ({              \
  typeof(a) __min1__ = (a);       \
  typeof(b) __min2__ = (b);       \
  (void)(&__min1__ == &__min2__); \
  __min1__ < __min2__ ? __min1__ : __min2__;})

替换为

template<class T>
T Max(T a, T b) { return a < b ? b : a; }

include

include guard

如果你的代码风格将违背此部分中的内容,请事先联系助教!

为防止一个文件被多次 include 而导致无法编译,我们需要写 include guard。

格式: <PROJECT>_<PATH>_<FILE>_H_

例子:

项目名为 awesome-project,头文件路径为 include/log.h

#ifndef AWESOME_PROJECT_LOG_H_
#define AWESOME_PROJECT_LOG_H_

...

#endif  // AWESOME_PROJECT_LOG_H_

defineendif 之间的是项目头文件的内容。

避免间接包含

如果你的代码风格将违背此部分中的内容,请事先联系助教!

例子:

<iostream> 中包含 <string>,但是如果你要使用 std::stringstd::cout,则你需要

#include <iostream>
#include <string>

尽量不使用前向声明 (forward declarations)

你未必一定要遵守此要求,但建议你尽量不使用前向声明。

前向声明 (forward declarations) 是指不带有定义的声明,典型的例子有:

// In a C++ source file:
class B;
void FuncInB();

前向声明可以减轻编译器的压力,但是在一些接口变更时有可能会出现问题,并且前向声明会影响一些语法检查工具。因此,尽量不要使用前向声明,除非实在过不了编译(比如存在循环引用的时候可以用前向声明解决)。

对于跨文件的函数,请在头文件 (.h) 中书写声明,而不是在源文件 (.cc) 中声明。

包含顺序

你未必一定要遵守此要求,但建议采用此顺序。

顺序按照以下所列之顺序,每个部分内按照字典序排序,每个部分之间空一行。

awesome-project/src/foo/internal/fooserver.cc 为例:

  • 当前源文件对应的头文件 ("foo/server/fooserver.h")
  • C 系统头文件 (<unistd.h>)
  • C++ 标准库头文件 (<vector>)
  • 其他库的头文件 ("base/basictypes.h")
  • 当前项目的头文件 ("awesome-project/include/log.h")

如:

#include "foo/server/fooserver.h"

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

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "foo/server/bar.h"

#include "awesome-project/include/log.h"

例外:需要条件包含某些头文件

#include "foo/public/fooserver.h"

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

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

不要使用 using namespace xxx;

任何情况下,你的大作业代码都不允许使用 using namespace 语句!

  • 相当于将 xxx 中的内容放到了全局;
  • 当项目复杂的时候,你的变量、函数可能会和其他 namespace 中的重名,导致代码需要重构;
  • 尤其在运算符重载时,会遇到莫名其妙的问题(因为 using namespace xxx 会优先在 xxx 中找,而你的声明不会在那个 namespace 中);
  • 如果觉得很不方便,请使用 using std::cin, std::cout, std::endl;(C++17 才支持 using 语句中加逗号,因此请使用 C++17)。

编码

如果你的代码风格将违背此部分中的内容,请事先联系助教!

  • 尽量使用 ACSII 编码(建议注释写英文);
  • 如必须使用非 ASCII 编码(例如需要写中文、emoji 等),请使用 UTF-8 编码。

不要使用非标准的扩展

如果你的代码风格将违背此部分中的内容,请事先联系助教!

不要使用非标准的扩展,如 #include <bits/stdc++.h>#pragma once