每个人都可能有自己的代码风格和格式,但如果一个项目中的所有人都遵循同一风格的话,这个项目就能更顺利地进行。每个人未必能同意下述的每一处格式规则,而且其中的不少规则需要一定时间的适应,但整个项目服从统一的编程风格是很重要的,只有这样才能让所有人轻松地阅读和理解代码。
每一行代码字符数不超过 120 注释行可以超过 120 个字符,但最大不超过 150
函数体行宽原则上不超过 80列 80 行限制事实上有助于避免代码可读性失控,比如超多重嵌套块,超多重函数调用等等。
尽量不使用非 ASCII 字符,使用时必须使用 UTF-8 编码。
即使是英文,也不应将用户界面的文本硬编码到源代码中,因此非 ASCII 字符应当很少被用到。特殊情况下可以适当包含此类字符。例如,代码分析外部数据文件时,可以适当硬编码数据文件中作为分隔符的非 ASCII 字符串;更常见的是 (不需要本地化的) 单元测试代码可能包含非 ASCII 字符串。此类情况下,应使用 UTF-8 编码,因为很多工具都可以理解和处理 UTF-8 编码。
只使用空格,每次缩进 4 个空格。(使用记事本打开检查是否使用空格还是制表符)
如果要在代码中使用制表符。你应该设置编辑器将制表符转为空格。
返回类型和函数名在同一行,参数也尽量放在同一行,如果放不下就对形参分行,分行方式如下 六、函数调用 一致。
函数看上去像这样:
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); // 4 space indent ... }如果同一行文本太多,放不下所有参数:
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, Type par_name3) { DoSomething(); // 4 space indent ... }甚至连第一个参数都放不下:
ReturnType LongClassName::ReallyReallyReallyLongFunctionName( Type par_name1, // 8 space indent Type par_name2, Type par_name3) { DoSomething(); // 4 space indent ... }注意以下几点:
使用好的参数名。只有在参数未被使用或者其用途非常明显时,才能省略参数名。如果返回类型和函数名在一行放不下,分行。如果返回类型与函数声明或定义分行了,不要缩进。左圆括号总是和函数名在同一行。函数名和左圆括号间永远没有空格。圆括号与参数间没有空格。左大括号 另起新行。右大括号总是单独位于函数 最后一行。所有形参应尽可能对齐。缺省缩进为 4 个空格。换行后的参数保持 8 个空格的缩进。或尽量对齐上一行参数。属性,和展开为属性的宏,写在函数声明或定义的最前面,即返回类型之前:
MUST_USE_RESULT bool IsOK();要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进 8 格。如果没有其它顾虑的话,尽可能精简行数,比如把多个参数适当地放在同一行里。
函数调用遵循如下形式:
bool retval = DoSomething(argument1, argument2, argument3);如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格:
bool retval = DoSomething(averyveryveryverylongargument1, argument2, argument3);参数也可以放在次行,缩进8格:
if(...) { DoSomething( argument1, argument2, // 8 空格缩进 argument3, argument4); }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 } };如果能增强可读性,简短的条件语句允许写在同一行。只有当语句简单并且没有使用 else 子句时使用:
if(x == kFoo) return new Foo(); if(x == kBar) return new Bar();但最好还是使用大括号:
if(x == kFoo) { return new Foo(); }如果语句有 else 分支则不允许:
// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行 if (x) DoThis(); else DoThat();不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较。 根据布尔类型的语义,零值为“假”(记为 FALSE),任何非零值都是“真”(记为TRUE)。TRUE 的值究竟是什么并没有统一的标准。例如 Visual C++ 将 TRUE 定义为1,而 Visual Basic 则将 TRUE 定义为 -1。 假设布尔变量名字为 flag,它与零值比较的标准 if 语句如下: if (flag) // 表示 flag 为真 if (!flag) // 表示 flag 为假 其它的用法都属于不良风格,例如: if (flag == TRUE) if (flag == 1) if (flag == FALSE) if (flag == 0)
不可将浮点变量用“==”或“!=”与任何数字比较。 千万要留意,无论是 float 还是 double 类型的变量,都有精度限制。所以一定要避 免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。 假设浮点变量的名字为 x,应当将 if(x == 0.0) // 隐含错误的比较 转化为 if((x>=-EPSINON) && (x<=EPSINON)) 其中 EPSINON 是允许的误差(即精度)。
应当将指针变量用“==”或“!=”与 NULL 比较。 指针变量的零值是“空”(记为 NULL)。尽管 NULL 的值与 0 相同,但是两者意义不 同。假设指针变量的名字为 p,它与零值比较的标准 if 语句如下: if(p == NULL) // p 与 NULL 显式比较,强调 p 是指针变量 if(p != NULL) 不要写成 if(p == 0) // 容易让人误解 p 是整型变量 if (p != 0) 或者 if(p) // 容易让人误解 p 是布尔变量 if(!p)
switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用 {} 或 continue。
switch 语句中的 case 块可以使用大括号也可以不用,取决于你的个人喜好。如果用的话,要按照下文所述的方法。
如果有不满足 case 条件的枚举值,switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning)。如果 default 应该永远执行不到,简单的加条 assert:
switch(var) // '('左边和右边没有空格,')'左边没有空格 { case 0: // 无空格 { ... // 8 空格缩进 break; } case 1: { ... break; } default: assert(false); }循环语句:
for(int i = 0; i < kSomeNumber; ++i) { printf("I take it back\n"); }空循环体应使用 {} 或 continue,而不是一个简单的分号。
while(condition) { // 反复循环直到条件失效. } for(int i = 0; i < kSomeNumber; ++i) // 可 - 空循环体. { } while(condition) continue; // 可 - contunue 表明没有逻辑. while(condition); // 差 - 看起来仅仅只是 while/loop 的部分之一.句点或箭头前后不要有空格。指针/地址操作符 (*, &) 之后不能有空格。
下面是指针和引用表达式的正确使用范例:
x = *p; p = &x; x = r.y; x = r->y;注意:
在访问成员时,句点或箭头前后没有空格。指针操作符 * 或 & 后没有空格。在声明指针变量或参数时,星号与类型或变量名紧挨都可以:
// 好, 空格前置,推荐使用 char *c; const string &str; // 好, 空格后置. char* c; const string& str; int x, *y; // 不允许 - 在多重声明中不能使用 & 或 * char * c; // 差 - * 两边都有空格 const string & str; // 差 - & 两边都有空格.在单个文件内要保持风格一致,所以,如果是修改现有文件,要遵照该文件的风格。
如果一个布尔表达式超过 标准行宽 120个字符,断行方式要统一一下。
下例中,逻辑与 (&&) 操作符总位于行尾:
if(this_one_thing > this_other_thing && a_third_thing == a_fourth_thing && yet_another && last_one) { ... }注意,上例的逻辑与 (&&) 操作符均位于行尾。
不要在 return 表达式里加上非必须的圆括号。
只有在写 x = expr 要加上括号的时候才在 return expr;里使用括号。
return result; // 返回值很简单, 没有圆括号. // 可以用圆括号把复杂表达式圈起来, 改善可读性. return (some_long_condition && another_condition); return (value); // 差 - 毕竟您从来不会写 var = (value); return(result); // 差 - return 可不是函数!预处理指令不要缩进,从行首开始。 即使预处理指令位于缩进代码块中,指令也应从行首开始。
// 好 - 指令从行首开始 if(lopsided_score) { #if DISASTER_PENDING // 正确 - 从行首开始 DropEverything(); # if NOTIFY // 非必要 - # 后跟空格 NotifyClient(); # endif #endif BackToNormal(); } // 差 - 指令缩进 if (lopsided_score) { #if DISASTER_PENDING // 差 - "#if" 应该放在行开头 DropEverything(); #endif // 差 - "#endif" 不要缩进 BackToNormal(); }访问控制块的声明依次序是 public:,protected:,private:,每个都缩进 2 个空格。
class MyClass : public OtherClass { public: // 注意有2个空格的缩进 MyClass(); // 标准的4空格缩进 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: 要缩进 1 个空格。除第一个关键词 (一般是 public) 外,其他关键词前要空一行。如果类比较小的话也可以不空。这些关键词后不要保留空行。public 放在最前面,然后是 protected,最后是 private。构造函数初始化列表放在同一行或按8格缩进并排多行。
// 如果所有变量能放在同一行: 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), // 8 space indent some_other_var_(var + 1) { DoSomething(); } // 右大括号 } 可以和左大括号 { 放在同一行 // 如果这样做合适的话 MyClass::MyClass(int var) : some_var_(var) {}命名空间内容不缩进。
命名空间不要增加额外的缩进层次,例如:
namespace { void foo() // 正确. 命名空间内没有额外的缩进. { ... } } // namespace不要在命名空间内缩进:
namespace { // 错, 缩进多余了. void foo() { ... } } // namespace声明嵌套命名空间时,每个命名空间都独立成行。
namespace foo { namespace bar {水平留白的使用根据在代码中的位置决定。永远不要在行尾添加没意义的留白。
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; } // 用括号把大括号与实现分开. ...这不仅仅是规则而是原则问题了:不在万不得已,不要使用空行。尤其是:两个函数定义之间的空行不要超过 2 行,函数体首尾不要留空行。函数体中也不要随意添加空行。
基本原则是:同一屏可以显示的代码越多,越容易理解程序的控制流。当然,过于密集的代码块和过于疏松的代码块同样难看,这取决于你的判断。但通常是垂直留白越少越好。
下面的规则可以让加入的空行更有效:
用于区别概念层次,如函数体内变量定义与调用函数用空行区分开。在多重 if-else 块里加空行或许有点可读性。几乎所有的代码都是 从上往下 读,从左往右 读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路用空白行区隔开来。
如下。在封包声明/导入声明和每个函数之间,都有空白行隔开。这条极其简单的规则极大地影响到代码的视觉外观。每个空白行都是一条线索,标识出新的独立概念。往下读代码,你的目光总会停留于空白行之后那一行。
package fitnesse.wikitext.widgets; import java.util.regex.*; public class BoldWidget extends ParentWidget { public static final String REGEXP = "'''.+?'''"; private static final Pattern pattern = Pattern.compile("'''(.+?)'''", Pattern.MULTILINE + Pattern.DOTALL ); public BoldWidget(ParentWidget parent, String text) throws Exception { super(parent); Matcher match = pattern.matcher(text); match.find(); addChildWidgets(match.group(1)); } public String render() throws Exception { StringBuffer html = new StringBuffer("</b>"); return html.toString(); } }如下。抽掉这些空白行,代码可读性减弱了不少。
package fitnesse.wikitext.widgets; import java.util.regex.*; public class BoldWidget extends ParentWidget { public static final String REGEXP = "'''.+?'''"; private static final Pattern pattern = Pattern.compile("'''(.+?)'''", Pattern.MULTILINE + Pattern.DOTALL ); public BoldWidget(ParentWidget parent, String text) throws Exception { super(parent); Matcher match = pattern.matcher(text); match.find(); addChildWidgets(match.group(1)); } public String render() throws Exception { StringBuffer html = new StringBuffer("</b>"); return html.toString(); } }在你不特意注视时,后果就更严重了。在第一个例子中,代码组会跳到你眼中,而第二个例子就像一堆乱麻。两段代码的区别,展示了垂直方向上区隔的作用。
• 由 Leung 写于 2019 年 10 月 30 日
• 参考:Google 开源项目风格指南——9. 格式 [代码整洁之道]