![]() 序C++用法很多,包容性也比较强。一个C++的工程可能包含了各种各样没见过的用法。本篇内容主要是参照谷歌C++标准规范,结合自身实际工作及经验,整理一份适合平时C++开发的规则,规范自身C++编程规范。详细内容可参考《Google C++风格指南》。 1 函数1.1 参数顺序总述 函数的参数顺序为: 输入参数在先, 后跟输出参数. 说明 C/C++ 中的函数参数或者是函数的输入, 或者是函数的输出, 或兼而有之. 输入参数通常是值参或 const引用, 输出参数或输入/输出参数则一般为非 const 指针. 在排列参数顺序时, 将所有的输入参数置于输出参数之前. 特别要注意, 在加入新参数时不要因为它们是新参数就置于参数列表最后, 而是仍然要按照前述的规则, 即将新的输入参数也置于输出参数之前. 这并非一个硬性规定. 输入/输出参数 (通常是类或结构体) 让这个问题变得复杂. 并且, 有时候为了其他函数保持一致, 你可能不得不有所变通. 1.2 编写简短函数总述 我们倾向于编写简短, 凝练的函数. 说明 长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割. 即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的 bug. 使函数尽量简短, 以便于他人阅读和修改代码. 在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码: 如果证实这些代码使用 / 调试起来很困难, 或者你只需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数. 1.3 引用参数总述 所有按引用传递的参数必须加上 const. 定义 在 C 语言中, 如果函数需要修改变量的值, 参数必须为指针, 如 int foo(int *pval). 在 C++ 中, 函数还可以声明为引用参数: int foo(int &val). 优点 定义引用参数可以防止出现 (*pval)++ 这样丑陋的代码. 引用参数对于拷贝构造函数这样的应用也是必需的. 同时也更明确地不接受空指针。 缺点 容易引起误解, 因为引用在语法上是值变量却拥有指针的语义。 结论 函数参数列表中, 所有引用参数都必须是 const: void Foo(const string &in, string *out); 1.4 函数重载总述 若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种. 这一规则也适用于构造函数。 定义 你可以编写一个参数类型为 const string& 的函数, 然后用另一个参数类型为 const char* 的函数对其进行重载: class MyClass { 优点 通过重载参数不同的同名函数, 可以令代码更加直观. 模板化代码需要重载, 这同时也能为使用者带来便利。 缺点 如果函数单靠不同的参数类型而重载 (acgtyrant 注:这意味着参数数量不变), 读者就得十分熟悉 C++ 五花八门的匹配规则, 以了解匹配过程具体到底如何. 另外, 如果派生类只重载了某个函数的部分变体, 继承语义就容易令人困惑. 结论 如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append(). 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector 以便使用者可以用 列表初始化 指定参数。 1.5 函数返回类型后置语法总述 只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法. 定义 C++ 现在允许两种不同的函数声明方式. 以往的写法是将返回类型置于函数名之前. 例如: int foo(int x); C++11 引入了这一新的形式. 现在可以在函数名前使用 auto 关键字, 在参数列表之后后置返回类型. 例如: auto foo(int x) -> int; 后置返回类型为函数作用域. 对于像 int 这样简单的类型, 两种写法没有区别. 但对于复杂的情况, 例如类域中的类型声明或者以函数参数的形式书写的类型, 写法的不同会造成区别. 优点 后置返回类型是显式地指定 Lambda 表达式 的返回值的唯一方式. 某些情况下, 编译器可以自动推导出 Lambda 表达式的返回类型, 但并不是在所有的情况下都能实现. 即使编译器能够自动推导, 显式地指定返回类型也能让读者更明了. 有时在已经出现了的函数参数列表之后指定返回类型, 能够让书写更简单, 也更易读, 尤其是在返回类型依赖于模板参数时. 例如: template <class T, class U> auto add(T t, U u) -> decltype(t + u); 对比下面的例子: template <class T, class U> decltype(declval<T&>() + declval<U&>()) add(T t, U u); 缺点 后置返回类型相对来说是非常新的语法, 而且在 C 和 Java 中都没有相似的写法, 因此可能对读者来说比较陌生. 在已有的代码中有大量的函数声明, 你不可能把它们都用新的语法重写一遍. 因此实际的做法只能是使用旧的语法或者新旧混用. 在这种情况下, 只使用一种版本是相对来说更规整的形式. 结论 在大部分情况下, 应当继续使用以往的函数声明写法, 即将返回类型置于函数名前. 只有在必需的时候 (如 Lambda 表达式) 或者使用后置语法能够简化书写并且提高易读性的时候才使用新的返回类型后置语法. 但是后一种情况一般来说是很少见的, 大部分时候都出现在相当复杂的模板代码中, 而多数情况下不鼓励写这样复杂的模板代码. 2 命名约定2.1 通用命名规则尽可能使用描述性的命名,不要用只有项目开发者才能理解的缩写,也不能通过砍掉字母来缩写单词(除非是程序员都熟悉的缩写)。 2.2 文件命名总述 文件名要全部小写,多个单词用下划线_或者‘-’连接。推荐使用“_”。 说明 可接受的文件名命名示例: my_useful_class.cpp 不要使用已经存在于 /usr/include 下的文件名 (Yang.Y 注: 即编译器搜索系统头文件的路径), 如 db.h. 通常应尽量让文件名更加明确. http_server_logs.h就比 logs.h 要好. 定义类时文件名一般成对出现, 如 foo_bar.h 和 foo_bar.cc, 对应于类 FooBar. 内联函数必须放在 .h 文件中. 如果内联函数比较短, 就直接放在 .h 中. 2.3 类型命名总述 类型名称的每个单词首字母均大写, 不包含下划线:MyExcitingClass, MyExcitingEnum. 说明 所有类型命名 —— 类, 结构体, 类型定义 (typedef), 枚举, 类型模板参数 —— 均使用相同约定, 即以大写字母开始, 每个单词首字母均大写, 不包含下划线. 例如: // 类和结构体 2.4 变量命名总述 变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以m_开始, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, m_class_data_member. 变量标识符与对应的类型标识符之间的区别应避免仅在于用小写字母写的初始字母不同情况。eg:Path path;是不可取的 说明 普通变量命名举例: string table_name; // 好 - 用下划线.string tablename; // 好 - 全小写. 类数据成员不管是静态还是非静态的,类数据成员都可以和普通变量一样,但要加m或者m_前缀。 若是m前缀则m后紧跟大写字母;若使用m_,后接小写字母。 m_用法:后接下划线和全小写 class TableInfo { m用法:后接首字母大写的独立单词 class TableInfo { 推荐使用第一种!! 结构体变量不管是静态的还是非静态的, 结构体数据成员都可以和普通变量一样, 不用像类成员那样接下划线。 struct UrlTableProperties { 全局变量总述 一般情况下禁止使用全局变量,非不得已情况下采用g_前缀,其他格式与普通变量相同。 说明 凡是需要采用全局变量extern,尽量都优化为get,set修改内部static静态变量。 int g_value; **注:**全局变量和静态全局变量均采用此命名格式。 指针变量总述 采用指针变量时要格外小心,尽量在声明时就初始化。避免程序中使用未初始化的野指针,从而导致程序崩溃。 说明 指针变量采用“驼峰”命名规则,即小写p前缀、大小写混合、单词首字母大写。 char name[] = “C++ Style”; 2.5 常量命名在程序代码中,所有固定的数值(可能除了- 1,0和1)都要用常量替换,也就是说,不使用“神奇的数字”(这些数字后来都不知道这个值代表什么)常量不允许使用小写字母,因此名称部分之间的下划线在这里是允许的。 const double PI = 3.14 2.6 函数命名总述 常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExctingFunction(); 说明 推荐使用,函数名每个单词首字母都需要大写,没有下划线。对于首字母缩写的单词,更倾向于将它们视作一个单词进行首字母大写 (例如, 写作StartRpc()而非StartRPC())。 AddTableEntry(); 取值和设值函数的命名与变量一致. 一般来说它们的名称与实际的成员变量对应, 但并不强制要求. 例如 int count()与void SetCount(int count)。 2.7 命名空间命名2.8 枚举命名总述 枚举的命名应当于常量和宏一致,以大写E字母开头,多个单词用下划线_连接。ESIZE_SEARCH。 说明 由于枚举的功能与宏功能类似,故规定枚举命名规则与宏命名一致。为避免与宏命名冲突,规定枚举命名以大写字母E作为前缀,使得枚举命名更加清晰。 enum EFormatStateExtPvr 2.9 宏命名总述 谷歌规则中推荐使用内联函数、枚举和常量代替宏的使用。 说明 宏的定义规则与枚举一样,只不过不需要任何字母前缀,大写以下划线_连接。 #define PI 3.14 宏定义中的参数,可以使用小写。 3 注释注释的重要性不亚于代码实现,好的注释能够让代码可读性更强。优秀的注释能减轻开发人员自身的负担,提高团队开发效率。当然也要注意:注释固然重要,但最好的注释就是代码本身,有意义的类型名和变量名,要远胜于用注释解释含糊不清的名字。 3.1 注释风格总述 使用// 或 /* */,都是允许的,只要统一即可。内容语言建议使用英文。 说明 本文推荐单行注释采用//,多行注释采用/* */形式。若工程中已经存在注释风格,需要与当前工程保持一致即可。 3.2 文件注释总述 在每一个文件的开头加入版权公告。 文件注释应包括版权、文件名、作者、版本、描述、日志、注释等内容。 说明 推荐采用以下格式,若工程已经存在模板,与其他文件保持一致即可。 法律公告和作者信息每个文件都应该包含许可证引用. 为项目选择合适的许可证版本.(比如, Apache 2.0, BSD, LGPL, GPL) 如果你对原始作者的文件做了重大修改, 请考虑删除原作者信息. 文件内容如果一个 .h 文件声明了多个概念, 则文件注释应当对文件的内容做一个大致的说明, 同时说明各概念之间的联系. 一个一到两行的文件注释就足够了, 对于每个概念的详细文档应当放在各个概念中, 而不是文件注释中. /* 3.3 类注释总述 每个类的定义都要附带一份注释,除非它的功能相当明显。 说明 类注释应当为读者理解如何使用与何时使用类提供足够的信息, 同时应当提醒读者在正确使用此类时应当考虑的因素. 如果类有任何同步前提, 请用文档说明. 如果该类的实例可被多线程访问, 要特别注意文档说明多线程环境下相关的规则和常量使用. 如果你想用一小段代码演示这个类的基本用法或通常用法, 放在类注释里也非常合适. 如果类的声明和定义分开了(例如分别放在了.h和.cpp文件中), 此时,描述类用法的注释应当和接口定义放在一起, 描述类的操作和实现的注释应当和实现放在一起. 3.4 函数注释总述 函数声明处的注释描述函数功能; 定义处的注释描述函数实现. 说明 函数声明基本上每个函数声明处前都应当加上注释, 描述函数的功能和用途. 只有在函数的功能简单而明显时才能省略这些注释(例如, 简单的取值和设值函数). 注释使用叙述式 (“Opens the file”) 而非指令式 (“Open the file”); 注释只是为了描述函数, 而不是命令函数做什么. 通常, 注释不会描述函数如何工作. 那是函数定义部分的事情. 经常被调用的函数声明处注释内容: /* 但也要避免啰嗦,或者对显而易见的内容进行说明. 下面的注释就没有必要加上 “否则返回 false”, 因为已经暗含其中了: // Returns true if the table cannot hold any more entries. 注释函数重载时, 注释的重点应该是函数中被重载的部分, 而不是简单的重复被重载的函数的注释. 多数情况下, 函数重载不需要额外的文档, 因此也没有必要加上注释. 注释构造/析构函数时, 切记读代码的人知道构造/析构函数的功能, 所以 “销毁这一对象” 这样的注释是没有意义的. 你应当注明的是注明构造函数对参数做了什么 (例如, 是否取得指针所有权) 以及析构函数清理了什么. 如果都是些无关紧要的内容, 直接省掉注释. 析构函数前没有注释是很正常的. 函数定义****如果函数的实现过程中用到了很巧妙的方式, 那么在函数定义处应当加上解释性的注释. 例如, 你所使用的编程技巧, 实现的大致步骤, 或解释如此实现的理由. 举个例子, 你可以说明为什么函数的前半部分要加锁而后半部分不需要. 不要从.h文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上. 3.5 变量注释总述 通常变量名本身足以很好说明变量用途。某些情况下,也需要额外注释说明。 说明 类数据成员每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果有非变量的参数(例如特殊值, 数据成员之间的关系, 生命周期等)不能够用类型与变量名明确表达, 则应当加上注释. 然而, 如果变量类型与变量名已经足以描述一个变量, 那么就不再需要加上注释. 特别地, 如果变量可以接受 NULL 或 -1 等警戒值, 须加以说明. 比如: private: 全局变量和数据成员一样, 所有全局变量也要注释说明含义及用途, 以及作为全局变量的原因. 比如: // The total number of tests cases that we run through in this regression test. 3.6 实现注释总述 对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释。 说明 代码前注释/* Divide result by two, taking into account that x 行注释比较隐晦的地方要在行尾加入注释。在行尾空两格进行注释。 比如: // If we have enough memory, mmap the data portion too. 3.7 标点,拼音和语法总述 注意标点, 拼写和语法; 写的好的注释比差的要易读的多。 说明 注释的通常写法是包含正确大小写和结尾句号的完整叙述性语句. 大多数情况下, 完整的句子比句子片段可读性更高. 短一点的注释, 比如代码行尾注释, 可以随意点, 但依然要注意风格的一致性。 虽然被别人指出该用分号时却用了逗号多少有些尴尬, 但清晰易读的代码还是很重要的. 正确的标点, 拼写和语法对此会有很大帮助。 3.8 TODO注释总述 对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释. TODO 注释要使用全大写的字符串 TODO, 在随后的圆括号里写上你的名字, 邮件地址, bug ID, 或其它身份标识和与这一 TODO 相关的 issue. 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 TODO 格式进行查找. 添加 TODO 注释并不意味着你要自己来修正, 因此当你加上带有姓名的 TODO 时, 一般都是写上自己的名字。 // TODO(kl@gmail.com): Use a "*" here for concatenation operator. 如果加 TODO 是为了在 “将来某一天做某事”, 可以附上一个非常明确的时间 “Fix by November 2005”), 或者一个明确的事项 (“Remove this code when all clients can handle XML responses.”)。 4 格式每个人都可能有自己的代码风格和格式, 但如果一个项目中的所有人都遵循同一风格的话, 这个项目就能更顺利地进行. 每个人未必能同意下述的每一处格式规则, 而且其中的不少规则需要一定时间的适应, 但整个项目服从统一的编程风格是很重要的, 只有这样才能让所有人轻松地阅读和理解代码。 4.1 行长度总述 每行长度字符数不超过80。 尽管这条规则具有争议,Linux源码的风格也是遵照这一规则,因此一致性更重要。 优点 提倡该原则的人认为强迫他们调整编辑器窗口大小是很野蛮的行为. 很多人同时并排开几个代码窗口, 根本没有多余的空间拉伸窗口. 大家都把窗口最大尺寸加以限定, 并且 80 列宽是传统标准。 缺点 反对该原则的人则认为更宽的代码行更易阅读. 80 列的限制是上个世纪 60 年代的大型机的古板缺陷; 现代设备具有更宽的显示屏, 可以很轻松地显示更多代码。 结论 如果无法在不伤害易读性的条件下进行断行, 那么注释行可以超过 80 个字符, 这样可以方便复制粘贴. 例如, 带有命令示例或 URL 的行可以超过 80 个字符。 包含长路径的 #include 语句可以超出80列。 4.2 非ASCII字符总述 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码。 4.3 空格还是制表位总述 只使用空格,每次缩进4个空格。 说明 我们使用空格缩进. 不要在代码中使用制表符。应该设置编辑器将制表符转为空格。Vim中Makefile需要使用制表位,Ctrl+V Tab即可出现制位表。 4.4 函数声明与定义总述 返回类型和函数名在同一行,参数也尽量放在同一行,如果放不下就对形参进行分行,分行方式与函数调用一致。 说明 函数看上去像这样: ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { 如果同一行文本太多, 放不下所有参数: ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, 甚至连第一个参数都放不下: ReturnType LongClassName::ReallyReallyReallyLongFunctionName( 注意以下几点:
4.5 条件语句总述 倾向于不在圆括号内使用空格. 关键字 if 和 else 另起一行. 说明
4.6 循环和开关选择语句总述 循环语句使用{}分段。尽管很多风格选择switch使用{}用来表明case之间不是连在一起的,但是这里采用linux内核风格,不推荐case使用{}包含分支,且case位置要与switch对齐。 说明 如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将给出 warning). 如果 default 应该永远执行不到, 简单的加条 assert: switch (var) { 4.7 指针和引用表达式总述 句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格. 说明 下面是指针和引用表达式的正确使用范例: x = *p; 注意:
// 好, 空格前置. 在单个文件内要保持风格一致, 所以, 如果是修改现有文件, 要遵照该文件的风格. 4.8 布尔表达式总述如果一个布尔表达式超过80行, 断行方式要统一一下. 说明 下例中, 逻辑与 (&&) 操作符总位于行尾: if (this_one_thing > this_other_thing && 4.9 预处理指令总述 预处理指令不要缩进, 从行首开始. 说明 即使预处理指令位于缩进代码块中, 指令也应从行首开始. // 好 - 指令从行首开始 4.10 类格式总述 访问控制块的声明依次序是 public:, protected:, private:, 每个都缩进 2 个空格。 说明 类声明的基本格式如下: class MyClass : public OtherClass { 注意事项:
4.11 水平留白总述 水平留白的使用根据在代码中的位置决定. 永远不要在行尾添加没意义的留白。 说明 通用void f(bool b) { // 左大括号前总是有空格. 添加冗余的留白会给其他人编辑时造成额外负担. 因此, 行尾不要留空格. 如果确定一行代码已经修改完毕, 将多余的空格去掉; 或者在专门清理空格时去掉。 循环和条件语句if (b) { // if 条件语句和循环语句关键字后均有空格. 操作符// 赋值运算符前后总是有空格. 模板和转换// 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有. 类数据成员与函数成员总述 一般情况下,在类中函数成员与数据成员之间要一行留白,便于查看。 说明 class Student { 4.12 垂直留白总述 垂直留白越少越好. 说明 这不仅仅是规则而是原则问题了: 不在万不得已, 不要使用空行. 尤其是: 两个函数定义之间的空行不要超过 2 行, 函数体首尾不要留空行, 函数体中也不要随意添加空行. 基本原则是: 同一屏可以显示的代码越多, 越容易理解程序的控制流. 当然, 过于密集的代码块和过于疏松的代码块同样难看, 这取决于你的判断. 但通常是垂直留白越少越好. 下面的规则可以让加入的空行更有效:
参考《Google C++风格指南》 持续更新中... 最后用心感悟,认真记录,写好每一篇文章,分享每一框干货。愿每一篇文章不负自己,不负看客! 猜你喜欢 详解 | Linux系统是如何实现存储并读写文件的? C++打怪 之 vector C++打怪 之 抽象类的使用 C语言编程规范_V1.1 更多文章内容包括但不限于C/C++、Linux、开发常用神器等,可进入开源519公众号聊天界面回复“文章目录” 或者 菜单栏选择“文章目录”查看。 ![]() ---------------------------------------------------------------------------------------------------------------------- 我们尊重原创,也注重分享,文章来源于微信公众号:开源519,建议关注公众号查看原文。如若侵权请联系qter@qter.org。 ---------------------------------------------------------------------------------------------------------------------- |