C++代码风格
本文档是 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
- 文件名:下划线 priority_queue.h
注释
如果你的代码风格将违背此部分中的内容,请事先联系助教!
- 注释可以大大提高代码的可读性;
- 尽管注释很重要,但是变量、函数等等最好可以从名称就直接能理解其作用;
- 多行注释的语法可以用
//
或/* */
,但请保持统一(通常建议//
); - 如果对象的名称无法体现其作用和用法,则需要写注释详细说明;
- 如果同时存在声明和定义,则在声明处写注释;
- 如果代码表面上的含义与其实际作用并不完全相同,需要注释详细说明(特别是函数内部和变量声明),如:
// 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 个字符原则)(你未必一定要遵守此要求,但请保持统一。)
public
,protected
和 private
关键词
你未必一定要遵守此要求,但请保持统一。
public:
,protected:
和private:
关键词仅 1 个空格缩进;- 对于
public:
,protected:
和private:
关键词,除第一个声明的外,剩下的关键词前需要有一行空行(很简短的类除外); public:
,protected:
和private:
关键词后无空行;- 声明顺序为(如果没有则省略)
public:
protected:
private:
声明顺序
你未必一定要遵守此要求,但请保持统一。
(依据 public
,protected
和 private
关键词章节,)一个类总是先 public:
,再 protected:
,最后 private:
;如没有某一部分,则省略。
各部分中,声明顺序为
- 类型和类型别名(
typedef
,using
,enum
, 内嵌类或结构体,友元类型) static
常量- 生成类的工厂函数
- 构造函数 (constructor) 和赋值运算符 (
operator=
) - 析构函数 (destructor)
- 其他函数(含静态和非静态成员函数,及友元函数)
- 成员变量(静态和非静态成员变量)
命名
你未必一定要遵守此要求,但请保持统一。
- 函数的命名与普通函数相同;
- 常量的命名与普通常量相同;
对于变量的命名
- 类中的变量命名为下划线命名法,并在最后加上一个下划线,如
class TableInfo { ... private: std::string table_name_; // 末尾有下划线 };
- 结构体中的命名为下划线命名法,最后无需下划线,如
struct UrlTableProperties { std::string name; };
继承与组合
多数情况下,请采用组合的方式——将需要的对象以成员的方式放在类中;只有当「当前类是一种基类」(如赋值语句是一种语句,则「赋值语句」类可以继承「语句」类)的时候,才可以采用继承。
采用继承时,请使用 public
继承。其他情况下,请使用组合。
允许使用多继承,但请注意每个基类都需要符合以上的要求。
虚函数
对于派生类的虚函数,请使用 override
或 final
关键词,不要使用 virtual
关键词。
类中成员的可见性
(你未必一定要遵守此要求,建议采用以下建议。)
为了防止各类危险的行为(如使用者继承此类),所有数据变量都应当是 private
的。
对于派生类需要调用,但派生类不能调用调用的成员函数,请使用 protected
。
类或结构体中的隐式转换
(你未必一定要遵守此要求,建议采用以下建议。)
隐式转换会导致函数调用时出现本不匹配的参数,因此尽可能避免隐式转换。
在具体的写代码的过程中,除了复制构造函数和移动构造函数外,其他能以单参数传入的构造函数必须加上 explicit
关键词(std::initializer_list
这类的参数也可以省略 explicit
关键词)。
其他
- 运算符重载参见运算符重载 (Operator Overloading) 章节;
- 函数重载参见函数重载 (Function Overloading) 章节。
变量
- 不要在一个语句中声明多个指针或引用类型的变量;(如果你的代码风格将违背此部分中的内容,请事先联系助教!)
- 尽可能在变量声明时赋值。(你未必一定要遵守此要求,但建议尽可能在变量声明时赋值。)
类型转换
(你未必一定要遵守此要求,建议采用以下建议。)
为避免出现二义性,建议使用 C++ 的转换,不建议使用 C 的转换。
除此以外,不建议使用 dynamic_cast
(不规范的运行时类型推断会导致代码很难维护),通常的设计不需要使用运行时类型推断来区分类,如果实在有需求,建议通过设置成员函数(建议返回值为 enum)来区分,并视需求采用 static_cast
。
空指针
如果你的代码风格将违背此部分中的内容,请事先联系助教!
对于指针,请使用 nullptr
;对于字符串,请使用 '\0'
。
不要使用 NULL
或 0
,这会导致出现隐式转换。
对于下面的代码:
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_
define
和 endif
之间的是项目头文件的内容。
避免间接包含
如果你的代码风格将违背此部分中的内容,请事先联系助教!
例子:
<iostream>
中包含 <string>
,但是如果你要使用 std::string
和 std::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
。