1.java
1.1 术语说明
在本文档中,除非另有说明:
1.术语class可表示一个普通类,枚举类,接口或是annotation类型(@interface)
2.术语comment只用来指代实现的注释(implementation comments),我们不使用“documentation comments”一词,而是用Javadoc。
其他的术语说明会偶尔在后面的文档出现。
1.2 指南说明
本文档中的示例代码并不作为规范。也就是说,虽然示例代码是遵循Google编程风格,但并不意味着这是展现这些代码的唯一方式。 示例中的格式选择不应该被强制定为规则。
2.1 文件名
源文件以其最顶层的类名来命名,大小写敏感,文件扩展名为.java。
2.2 文件编码:UTF-8
源文件编码格式为UTF-8。
2.3 特殊字符
2.3.1 空白字符
除了行结束符序列,ASCII水平空格字符(0x20,即空格)是源文件中唯一允许出现的空白字符,这意味着:
1.所有其它字符串中的空白字符都要进行转义。
2.制表符不用于缩进。
2.3.2 特殊转义序列
对于具有特殊转义序列的任何字符(\b, \t, \n, \f, \r, \“, \‘及\),我们使用它的转义序列,而不是相应的八进制(比如\012)或Unicode(比如\u000a)转义。
2.3.3 非ASCII字符
对于剩余的非ASCII字符,是使用实际的Unicode字符(比如∞),还是使用等价的Unicode转义符(比如\u221e),取决于哪个能让代码更易于阅读和理解。
Tip: 在使用Unicode转义符或是一些实际的Unicode字符时,建议做些注释给出解释,这有助于别人阅读和理解。
源文件结构
一个源文件包含(按顺序地):
1.许可证或版权信息(如有需要)
2.package语句
3.import语句
4.一个顶级类(只有一个)
以上每个部分之间用一个空行隔开。
3.1 许可证或版权信息
如果一个文件包含许可证或版权信息,那么它应当被放在文件最前面。
3.2 package语句
package语句不换行,列限制(4.4节)并不适用于package语句。(即package语句写在一行里)
3.3 import语句
3.3.1 import不要使用通配符
即,不要出现类似这样的import语句:import java.util.*;
3.3.2 不要换行
import语句不换行,列限制(4.4节)并不适用于import语句。(每个import语句独立成行)
3.3.3 顺序和间距
import语句可分为以下几组,按照这个顺序,每组由一个空行分隔:
1.所有的静态导入独立成组
2.com.googleimports(仅当这个源文件是在com.google包下)
3.第三方的包。每个顶级包为一组,字典序。例如:android, com, junit, org, sun
4.javaimports
5.javaximports
组内不空行,按字典序排列。
3.4 类声明
3.4.1 只有一个顶级类声明
每个顶级类都在一个与它同名的源文件中(当然,还包含.java后缀)。
3.4.2 类成员顺序
类的成员顺序对易学性有很大的影响,但这也不存在唯一的通用法则。不同的类对成员的排序可能是不同的。 最重要的一点,每个类应该以某种逻辑去排序它的成员,维护者应该要能解释这种排序逻辑。比如, 新的方法不能总是习惯性地添加到类的结尾,因为这样就是按时间顺序而非某种逻辑来排序的。
3.4.2.1 重载:永不分离
当一个类有多个构造函数,或是多个同名方法,这些函数/方法应该按顺序出现在一起,中间不要放进其它函数/方法。
格式
术语说明:块状结构(block-like construct)指的是一个类,方法或构造函数的主体。需要注意的是,数组初始化中的初始值可被选择性地视为块状结构。
4.1 大括号
4.1.1 使用大括号(即使是可选的)
大括号与if, else, for, do, while语句一起使用,即使只有一条语句(或是空),也应该把大括号写上。
4.1.2 非空块:K & R 风格
对于非空块和块状结构,大括号遵循Kernighan和Ritchie风格(Egyptian brackets):
1,左大括号前不换行
,2,左大括号后换行
3,右大括号前换行
4,如果右大括号是一个语句、函数体或类的终止,则右大括号后换行; 否则不换行。例如,如果右大括号后面是else或逗号,则不换行。
4.1.3 空块:可以用简洁版本
一个空的块状结构里什么也不包含,大括号可以简洁地写成{},不需要换行。例外:如果它是一个多块语句的一部分(if/else 或try/catch/finally) ,即使大括号内没内容,右大括号也要换行。
4.2 块缩进:2个空格
每当开始一个新的块,缩进增加2个空格,当块结束时,缩进返回先前的缩进级别。缩进级别适用于代码和注释。
4.3一行一个语句,每个语句要换行
4.4 列限制:80或100
一个项目可以选择一行80个字符或100个字符的列限制,除了下述例外,任何一行如果超过这个字符数限制,必须自动换行。
例外:
1.不可能满足列限制的行(例如,Javadoc中的一个长URL,或是一个长的JSNI方法参考)。
2.package和import语句(见3.2节和3.3节)。
3.注释中那些可能被剪切并粘贴到shell中的命令行。
4.5 自动换行
术语说明:一般情况下,一行长代码为了避免超出列限制(80或100个字符)而被分为多行,我们称之为自动换行(line-wrapping)。
我们并没有全面,确定性的准则来决定在每一种情况下如何自动换行。很多时候,对于同一段代码会有好几种有效的自动换行方式。
Tip: 提取方法或局部变量可以在不换行的情况下解决代码过长的问题(是合理缩短命名长度吧)
4.5.1 从哪里断开
自动换行的基本准则是:更倾向于在更高的语法级别处断开。
1.如果在非赋值运算符处断开,那么在该符号前断开(比如+,它将位于下一行)。注意:这一点与Google其它语言的编程风格不同(如C++和JavaScript)。 这条规则也适用于以下“类运算符”符号:点分隔符(.),类型界限中的&(
2.如果在赋值运算符处断开,通常的做法是在该符号后断开(比如=,它与前面的内容留在同一行)。这条规则也适用于foreach语句中的分号。
3.方法名或构造函数名与左括号留在同一行。
4.逗号(,)与其前面的内容留在同一行。
4.5.2 自动换行时缩进至少+4个空格
自动换行时,第一行后的每一行至少比第一行多缩进4个空格。
当存在连续自动换行时,缩进可能会多缩进不只4个空格(语法元素存在多级时)。一般而言,两个连续行使用相同的缩进当且仅当它们开始于同级语法元素。
4.6 空白
4.6.1 垂直空白
以下情况需要使用一个空行:
1.类内连续的成员之间:字段,构造函数,方法,嵌套类,静态初始化块,实例初始化块。
?例外:两个连续字段之间的空行是可选的,用于字段的空行主要用来对字段进行逻辑分组。
2.在函数体内,语句的逻辑分组间使用空行。
3.类内的第一个成员前或最后一个成员后的空行是可选的(既不鼓励也不反对这样做,视个人喜好而定)。
4.要满足本文档中其他节的空行要求(比如3.3节:import语句)
多个连续的空行是允许的,但没有必要这样做(我们也不鼓励这样做)。
4.6.2 水平空白
除了语言需求和其它规则,并且除了文字,注释和Javadoc用到单个空格,单个ASCII空格也出现在以下几个地方:
1.分隔任何保留字与紧随其后的左括号(()(如if,for catch等)。
2.分隔任何保留字与其前面的右大括号(})
3.在任何左大括号前({),两个例外:
?@SomeAnnotation({a,b})(不使用空格)。
?String[][]x = foo;(大括号间没有空格,见下面的Note)。
4.在任何二元或三元运算符的两侧。这也适用于以下“类运算符”符号:
?类型界限中的&(
?catch块中的管道符号(catch (FooException | BarException e)。
?foreach语句中的分号。
5.在, : ;及右括号())后
6.如果在一条语句后做注释,则双斜杠(//)两边都要空格。这里可以允许多个空格,但没有必要。
7.类型和变量之间:List list。
8.数组初始化中,大括号内的空格是可选的,即new int[] {5, 6}和new int[] { 5, 6 }都是可以的。
Note:这个规则并不要求或禁止一行的开关或结尾需要额外的空格,只对内部空格做要求。
4.6.3 水平对齐:不做要求
术语说明:水平对齐指的是通过增加可变数量的空格来使某一行的字符与上一行的相应字符对齐。
这是允许的(而且在不少地方可以看到这样的代码),但Google编程风格对此不做要求。即使对于已经使用水平对齐的代码,我们也不需要去保持这种风格。
4.7 用小括号来限定组:推荐
除非作者和reviewer都认为去掉小括号也不会使代码被误解,或是去掉小括号能让代码更易于阅读,否则我们不应该去掉小括号。 我们没有理由假设读者能记住整个Java运算符优先级表。
4.8 具体结构
4.8.1 枚举类
枚举常量间用逗号隔开,换行可选。
没有方法和文档的枚举类可写成数组初始化的格式:
privateenum Suit { CLUBS, HEARTS, SPADES, DIAMONDS }
由于枚举类也是一个类,因此所有适用于其它类的格式规则也适用于枚举类。
4.8.2 变量声明
4.8.2.1 每次只声明一个变量
不要使用组合声明,比如int a, b;。
4.8.2.2 需要时才声明,并尽快进行初始化
不要在一个代码块的开头把局部变量一次性都声明了(这是c语言的做法),而是在第一次需要使用它时才声明。 局部变量在声明时最好就进行初始化,或者声明后尽快进行初始化。
4.8.3 数组
4.8.3.1 数组初始化:可写成块状结构
数组初始化可以写成块状结构,
4.8.4 switch语句
术语说明:switch块的大括号内是一个或多个语句组。每个语句组包含一个或多个switch标签(case FOO:或default:),后面跟着一条或多条语句。
4.8.4.1 缩进
与其它块状结构一致,switch块中的内容缩进为2个空格。
每个switch标签后新起一行,再缩进2个空格,写下一条或多条语句。
4.8.4.2Fall-through:注释
在一个switch块内,每个语句组要么通过break, continue, return或抛出异常来终止,要么通过一条注释来说明程序将继续执行到下一个语句组, 任何能表达这个意思的注释都是OK的(典型的是用//fall through)。这个特殊的注释并不需要在最后一个语句组(一般是default)中出现。
4.8.4.3 default的情况要写出来
每个switch语句都包含一个default语句组,即使它什么代码也不包含。
4.8.5 注解(Annotations)
注解紧跟在文档块后面,应用于类、方法和构造函数,一个注解独占一行。这些换行不属于自动换行(第4.5节,自动换行),因此缩进级别不变。
4.8.6 注释
4.8.6.1 块注释风格
块注释与其周围的代码在同一缩进级别。它们可以是/* ... */风格,也可以是// ...风格。对于多行的/* ... */注释,后续行必须从*开始, 并且与前一行的*对齐。
4.8.7 Modifiers
类和成员的modifiers如果存在,则按Java语言规范中推荐的顺序出现。
5.1 对所有标识符都通用的规则
标识符只能使用ASCII字母和数字,因此每个有效的标识符名称都能匹配正则表达式\w+。
在Google其它编程语言风格中使用的特殊前缀或后缀,如name_, mName, s_name和kName,在Java编程风格中都不再使用。
5.2 标识符类型的规则
5.2.1 包名
包名全部小写,连续的单词只是简单地连接起来,不使用下划线。
5.2.2 类名
类名都以UpperCamelCase风格编写。
类名通常是名词或名词短语,接口名称有时可能是形容词或形容词短语。现在还没有特定的规则或行之有效的约定来命名注解类型。
测试类的命名以它要测试的类的名称开始,以Test结束。例如,HashTest或HashIntegrationTest。
5.2.3 方法名
方法名都以lowerCamelCase风格编写。
方法名通常是动词或动词短语。
下划线可能出现在JUnit测试方法名称中用以分隔名称的逻辑组件。一个典型的模式是:test
5..4 常量名
常量名命名模式为CONSTANT_CASE,全部字母大写,用下划线分隔单词。那,到底什么算是一个常量?
每个常量都是一个静态final字段,但不是所有静态final字段都是常量。在决定一个字段是否是一个常量时, 考虑它是否真的感觉像是一个常量。例如,如果任何一个该实例的观测状态是可变的,则它几乎肯定不会是一个常量。 只是永远不打算改变对象一般是不够的,它要真的一直不变才能将它示为常量。
5.2.5 非常量字段名
非常量字段名以lowerCamelCase风格编写。
这些名字通常是名词或名词短语。
5.2.6 参数名
参数名以lowerCamelCase风格编写。
参数应该避免用单个字符命名。
5.2.7 局部变量名
局部变量名以lowerCamelCase风格编写,比起其它类型的名称,局部变量名可以有更为宽松的缩写。
虽然缩写更宽松,但还是要避免用单字符进行命名,除了临时变量和循环变量。
即使局部变量是final和不可改变的,也不应该把它示为常量,自然也不能用常量的规则去命名它。
5.2.8 类型变量名
类型变量可用以下两种风格之一进行命名:
单个的大写字母,后面可以跟一个数字(如:E,T, X, T2)。
以类命名方式(5.2.2节),后面加个大写的T(如:RequestT, FooBarT)。
5.3 驼峰式命名法(CamelCase)
驼峰式命名法分大驼峰式命名法(UpperCamelCase)和小驼峰式命名法(lowerCamelCase)。 有时,我们有不只一种合理的方式将一个英语词组转换成驼峰形式,如缩略语或不寻常的结构(例如"IPv6"或"iOS")。Google指定了以下的转换方案。
名字从散文形式(prose form)开始:
1.把短语转换为纯ASCII码,并且移除任何单引号。例如:"Müller’salgorithm"将变成"Muellers algorithm"。
2.把这个结果切分成单词,在空格或其它标点符号(通常是连字符)处分割开。
?推荐:如果某个单词已经有了常用的驼峰表示形式,按它的组成将它分割开(如"AdWords"将分割成"ad words")。 需要注意的是"iOS"并不是一个真正的驼峰表示形式,因此该推荐对它并不适用。
3.现在将所有字母都小写(包括缩写),然后将单词的第一个字母大写:
?每个单词的第一个字母都大写,来得到大驼峰式命名。
?除了第一个单词,每个单词的第一个字母都大写,来得到小驼峰式命名。
4.最后将所有的单词连接起来得到一个标识符。
6.1 @Override:能用则用
只要是合法的,就把@Override注解给用上。
6.2 捕获的异常:不能忽视
除了下面的例子,对捕获的异常不做响应是极少正确的。(典型的响应方式是打印日志,或者如果它被认为是不可能的,则把它当作一个AssertionError重新抛出。)
如果它确实是不需要在catch块中做任何响应,需要做注释加以说明。
6.3 静态成员:使用类进行调用
使用类名调用静态的类成员,而不是具体某个对象或表达式。
7.1 格式
7.1.1 一般形式
Javadoc块的基本格式如下所示:
/**
* Multiple lines of Javadoc text are writtenhere,
* wrapped normally...
*/
publicint method(String p1) { ... }
或者是以下单行形式:
/** Anespecially short bit of Javadoc. */
基本格式总是OK的。当整个Javadoc块能容纳于一行时(且没有Javadoc标记@XXX),可以使用单行形式。
7.1.2 段落
空行(即,只包含最左侧星号的行)会出现在段落之间和Javadoc标记(@XXX)之前(如果有的话)。 除了第一个段落,每个段落第一个单词前都有标签
,并且它和第一个单词间没有空格。
7.1.3 Javadoc标记
标准的Javadoc标记按以下顺序出现:@param, @return, @throws, @deprecated, 前面这4种标记如果出现,描述都不能为空。当描述无法在一行中容纳,连续行需要至少再缩进4个空格。
7.2 摘要片段
每个类或成员的Javadoc以一个简短的摘要片段开始。这个片段是非常重要的,在某些情况下,它是唯一出现的文本,比如在类和方法索引中。
这只是一个小片段,可以是一个名词短语或动词短语,但不是一个完整的句子。它不会以A {@code Foo} is a...或This method returns...开头, 它也不会是一个完整的祈使句,如Save the record...。然而,由于开头大写及被加了标点,它看起来就像是个完整的句子。
Tip:一个常见的错误是把简单的Javadoc写成/**@return the customer ID */,这是不正确的。它应该写成/** Returns the customer ID. */。
7.3 哪里需要使用Javadoc
至少在每个public类及它的每个public和protected成员处使用Javadoc。
7.3.1 例外:不言自明的方法
对于简单明显的方法如getFoo,Javadoc是可选的(即,是可以不写的)。这种情况下除了写“Returns the foo”,确实也没有什么值得写了。
单元测试类中的测试方法可能是不言自明的最常见例子了,我们通常可以从这些方法的描述性命名中知道它是干什么的,因此不需要额外的文档说明。
Tip:如果有一些相关信息是需要读者了解的,那么以上的例外不应作为忽视这些信息的理由。例如,对于方法名getCanonicalName, 就不应该忽视文档说明,因为读者很可能不知道词语canonical name指的是什么。
7.3.2 例外:重载
如果一个方法重载了超类中的方法,那么Javadoc并非必需的。
7.3.3 可选的Javadoc
对于包外不可见的类和方法,如有需要,也是要使用Javadoc的。如果一个注释是用来定义一个类,方法,字段的整体目的或行为, 那么这个注释应该写成Javadoc,这样更统一更友好。
c++
1. 头文件
通常每一个 .cc 文件都有一个对应的 .h 文件.也有一些常见例外, 如单元测试代码和只包含 main() 函数的 .cc 文件.
正确使用头文件可令代码在可读性、文件大小和性能上大为改观.
下面的规则将引导你规避使用头文件时的各种陷阱.
1.1.Self-contained 头文件
头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 .h 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法.
所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有 1.2. #define 保护,统统包含它所需要的其它头文件,也不要求定义任何特别 symbols.
不过有一个例外,即一个文件并不是 self-contained 的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用 .inc 文件扩展名。
如果 .h 文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 .cc 文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的 -inl.h 文件里(译者注:过去该规范曾提倡把定义放到 -inl.h 里过)。
有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的 .cc 文件里。
1.2. #define 保护
所有头文件都应该使用 #define 来防止头文件被多重包含, 命名格式当是:
为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径.头文件foo/src/bar/baz.h
1.3. 前置声明
尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。
定义:
所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义.
优点:
1.前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
2.前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。
缺点:
1.前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
2.前置声明来自命名空间 std:: 的symbol 时,其行为未定义。
很难判断什么时候该用前置声明,什么时候该用 #include 。极端情况下,用前置声明代替 includes 甚至都会暗暗地改变代码的含义:
如果 #include 被 B 和D 的前置声明替代, test() 就会调用 f(void*) . * 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。 * 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂.
结论:
1.尽量避免前置声明那些定义在其他项目中的实体.
2.函数:总是使用 #include.
3.类模板:优先使用 #include.
至于什么时候包含头文件,参见 name-and-order-of-includes。
1.4. 内联函数
只有当函数只有 10 行甚至更少时才将其定义为内联函数.
定义:
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
优点:
只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点:
滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论:
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要;比如虚函数和递归函数就不会被正常内联. 通常,递归函数不应该声明成内联函数.(YuleFox注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
1.5. #include 的路径及顺序
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库,C++ 库, 其他库的.h, 本项目内的 .h.
项目内头文件应按照项目源代码目录树结构排列, 避免使用UNIX 特殊的快捷目录 . (当前目录)或 .. (上级目录). 例如,google-awesome-project/src/base/logging.h 应该按如下方式包含:
#include"base/logging.h"
又如, dir/foo.cc 的主要作用是实现或测试 dir2/foo2.h 的功能,foo.cc 中包含头文件的次序如下:
1.dir2/foo2.h(优先位置, 详情如下)
2.C 系统文件
3.C++ 系统文件
4.其他库的 .h 文件
5.本项目内 .h 文件
这种优先的顺序排序保证当 dir2/foo2.h 遗漏某些必要的库时, dir/foo.cc 或dir/foo_test.cc 的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。
dir/foo.cc和 dir2/foo2.h 通常位于同一目录下 (如base/basictypes_unittest.cc 和base/basictypes.h), 但也可以放在不同目录下.
按字母顺序对头文件包含进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。
您所依赖的 symbols 被哪些头文件所定义,您就应该包含(include)哪些头文件,forward-declaration 情况除外。比如您要用到 bar.h 中的某个symbol, 哪怕您所包含的 foo.h 已经包含了 bar.h, 也照样得包含 bar.h, 除非 foo.h 有明确说明它会自动向您提供 bar.h 中的symbol. 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 foo.cc 只包含 foo.h 就够了,不用再管后者所包含的其它内容。
例外:
有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立。
译者 (YuleFox) 笔记
1.避免多重包含是学编程时最基本的要求;
2.前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;
3.内联函数的合理使用可提高代码执行效率;
4.-inl.h 可提高代码可读性 (一般用不到吧:D);
5.标准化函数参数顺序可以提高可读性和易维护性 (对函数参数的堆栈空间有轻微影响, 我以前大多是相同类型放在一起);
6.包含文件的名称使用 . 和.. 虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理, 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在 “最需要编译” (对应源文件处 :D) 的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了.
2. 作用域
2.1. 名字空间
鼓励在 .cc 文件内使用匿名名字空间. 使用具名的名字空间时, 其名称可基于项目名或相对路径. 禁止使用using 指示(using-directive)。禁止使用内联命名空间(inline namespace)。
定义:
名字空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.
优点:
虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 名字空间在这基础上又封装了一层.
举例来说, 两个不同项目的全局作用域都有一个类 Foo, 这样在编译或运行时造成冲突. 如果每个项目将代码置于不同名字空间中, project1::Foo 和project2::Foo 作为不同符号自然不会冲突.
内联命名空间会自动把内部的标识符放到外层作用域,
缺点:
名字空间具有迷惑性, 因为它们和类一样提供了额外的 (可嵌套的) 命名轴线.
命名空间很容易令人迷惑,毕竟它们不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。
在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)).
结论:
根据下文将要提到的策略合理使用命名空间.
2.1.1. 匿名名字空间
在 .cc 文件中, 允许甚至鼓励使用匿名名字空间, 以避免运行时的命名冲突:
namespace{ // .cc 文件中
// 名字空间的内容无需缩进
enum {kUNUSED, kEOF, kERROR }; // 经常使用的符号
boolAtEof() { return pos_ == kEOF; } // 使用本名字空间内的符号 EOF
} //namespace
然而, 与特定类关联的文件作用域声明在该类中被声明为类型, 静态数据成员或静态成员函数, 而不是匿名名字空间的成员. 如上例所示,匿名空间结束时用注释 // namespace 标识.
不要在 .h 文件中使用匿名名字空间.
2.1.2. 具名的名字空间
具名的名字空间使用方式如下:
用名字空间把文件包含, gflags 的声明/定义,以及类的前置声明以外的整个源文件封装起来, 以区别于其它名字空间。
2.2. 嵌套类
当公有嵌套类作为接口的一部分时, 虽然可以直接将他们保持在全局作用域中, 但将嵌套类的声明置于 2.1. 名字空间 内是更好的选择.
定义: 在一个类内部定义另一个类; 嵌套类也被称为 成员类(member class).
优点:
当嵌套 (或成员)类只被外围类使用时非常有用; 把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类. 嵌套类可以在外围类中做前置声明, 然后在 .cc 文件中定义, 这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关.
缺点:
嵌套类只能在外围类的内部做前置声明. 因此,任何使用了 Foo::Bar* 指针的头文件不得不包含类 Foo 的整个声明.
结论:
不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项.
2.3. 非成员函数、静态成员函数和全局函数
使用静态成员函数或名字空间内的非成员函数, 尽量不要用裸的全局函数.
优点:
某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在名字空间内可避免污染全局作用域.
缺点:
将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此.
结论:
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用2.1. 名字空间。
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内.
如果你必须定义非成员函数, 又只是在.cc 文件中使用它, 可使用匿名namespaces`或 ``static` 链接关键字 (如static int Foo() {...}) 限定其作用域.
2.4. 局部变量
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值,
2.5. 静态和全局变量
禁止使用 class 类型的静态或全局变量:它们会导致难以发现的 bug 和不确定的构造和析构函数调用顺序。不过 constexpr 变量除外,毕竟它们又不涉及动态初始化或析构。
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和float, 以及 POD 类型的指针、数组和结构体。
静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是不确定的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数不涉及(比如 getenv() 或getpid())不涉及任何全局变量。(函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。)
同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从 main() 返回还是对 exit() 的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。
改善以上析构问题的办法之一是用 quick_exit() 来代替 exit() 并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行 atexit() 所绑定的任何 handlers. 如果您想在执行 quick_exit() 来中断时执行某 handler(比如刷新log),您可以把它绑定到 _at_quick_exit(). 如果您想在 exit() 和quick_exit() 都用上该 handler, 都绑定上去。
综上所述,我们只允许 POD 类型的静态变量,即完全禁用 vector (使用C 数组替代) 和string (使用 const char [])。
如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
3. 类
类是 C++ 中代码的基本单元. 显然,它们被广泛使用. 本节列举了在写一个类时的主要注意事项.
3.1. 构造函数的职责
不要在构造函数中进行复杂的初始化 (尤其是那些有可能失败或者需要调用虚函数的初始化).
定义:
在构造函数体中进行初始化操作.
优点:
排版方便, 无需担心类是否已经初始化.
缺点:
在构造函数中执行操作引起的问题有:
1.构造函数中很难上报错误, 不能使用异常.
2.操作失败会造成对象初始化失败,进入不确定状态.
3.如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患.
4.如果有人创建该类型的全局变量 (虽然违背了上节提到的规则), 构造函数将先 main() 一步被调用, 有可能破坏构造函数中暗含的假设条件. 例如, gflags 尚未初始化.
结论:
构造函数不得调用虚函数, 或尝试报告一个非致命错误. 如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的 Init() 方法或使用工厂模式.
3.2. 初始化
如果类中定义了成员变量, 则必须在类中为每个类提供初始化函数或定义一个构造函数. 若未声明构造函数, 则编译器会生成一个默认的构造函数, 这有可能导致某些成员未被初始化或被初始化为不恰当的值.
定义:
new 一个不带参数的类对象时, 会调用这个类的默认构造函数. 用 new[] 创建数组时, 默认构造函数则总是被调用. 在类成员里面进行初始化是指声明一个成员变量的时候使用一个结构例如 int _count = 17 或者 string _name{"abc"} 来替代 int _count 或者string _name 这样的形式.
优点:
用户定义的默认构造函数将在没有提供初始化操作时将对象初始化. 这样就保证了对象在被构造之时就处于一个有效且可用的状态, 同时保证了对象在被创建时就处于一个显然”不可能”的状态, 以此帮助调试.
缺点:
对代码编写者来说, 这是多余的工作.
如果一个成员变量在声明时初始化又在构造函数中初始化, 有可能造成混乱, 因为构造函数中的值会覆盖掉声明中的值.
结论:
简单的初始化用类成员初始化完成, 尤其是当一个成员变量要在多个构造函数里用相同的方式初始化的时候.
如果你的类中有成员变量没有在类里面进行初始化, 而且没有提供其它构造函数, 你必须定义一个 (不带参数的)默认构造函数. 把对象的内部状态初始化成一致 / 有效的值无疑是更合理的方式.
这么做的原因是: 如果你没有提供其它构造函数, 又没有定义默认构造函数, 编译器将为你自动生成一个. 编译器生成的构造函数并不会对对象进行合理的初始化.
如果你定义的类继承现有类, 而你又没有增加新的成员变量, 则不需要为新类定义默认构造函数.
3.3. 显式构造函数
对单个参数的构造函数使用 C++ 关键字 explicit.
定义:
通常, 如果构造函数只有一个参数, 可看成是一种隐式转换. 打个比方,如果你定义了 Foo::Foo(string name), 接着把一个字符串传给一个以 Foo 对象为参数的函数, 构造函数 Foo::Foo(string name) 将被调用, 并将该字符串转换为一个 Foo 的临时对象传给调用函数. 看上去很方便, 但如果你并不希望如此通过转换生成一个新对象的话, 麻烦也随之而来. 为避免构造函数被调用造成隐式转换, 可以将其声明为 explicit.
除单参数构造函数外, 这一规则也适用于除第一个参数以外的其他参数都具有默认参数的构造函数, 例如Foo::Foo(string name, int id = 42).
优点:
避免不合时宜的变换.
缺点:
无
结论:
所有单参数构造函数都必须是显式的. 在类定义中,将关键字 explicit 加到单参数构造函数前: explicit Foo(string name);
例外: 在极少数情况下, 拷贝构造函数可以不声明成 explicit. 作为其它类的透明包装器的类也是特例之一. 类似的例外情况应在注释中明确说明.
最后, 只有std::initializer_list 的构造函数可以是非 explicit, 以允许你的类型结构可以使用列表初始化的方式进行赋值.
3.4可拷贝类型和可移动类型
如果你的类型需要, 就让它们支持拷贝 / 移动. 否则,就把隐式产生的拷贝和移动函数禁用.
定义:
可拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一对象的值, 同时不改变源对象的值. 对于用户定义的类型, 拷贝操作一般通过拷贝构造函数与拷贝赋值操作符定义. string 类型就是一个可拷贝类型的例子.
可移动类型允许对象在初始化时得到来自相同类型的临时对象的值, 或在赋值时被赋予相同类型的临时对象的值 (因此所有可拷贝对象也是可移动的). std::unique_ptr
拷贝 / 移动构造函数在某些情况下会被编译器隐式调用. 例如,通过传值的方式传递对象.
优点:
可移动及可拷贝类型的对象可以通过传值的方式进行传递或者返回, 这使得API 更简单, 更安全也更通用. 与传指针和引用不同, 这样的传递不会造成所有权, 生命周期, 可变性等方面的混乱, 也就没必要在协议中予以明确. 这同时也防止了客户端与实现在非作用域内的交互, 使得它们更容易被理解与维护. 这样的对象可以和需要传值操作的通用 API 一起使用, 例如大多数容器.
拷贝 / 移动构造函数与赋值操作一般来说要比它们的各种替代方案, 比如Clone(), CopyFrom() or Swap(), 更容易定义,因为它们能通过编译器产生, 无论是隐式的还是通过 = 默认. 这种方式很简洁, 也保证所有数据成员都会被复制. 拷贝与移动构造函数一般也更高效, 因为它们不需要堆的分配或者是单独的初始化和赋值步骤, 同时,对于类似省略不必要的拷贝这样的优化它们也更加合适.
移动操作允许隐式且高效地将源数据转移出右值对象. 这有时能让代码风格更加清晰.
缺点:
许多类型都不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理. 为基类提供拷贝 / 赋值操作是有害的, 因为在使用它们时会造成对象切割. 默认的或者随意的拷贝操作实现可能是不正确的, 这往往导致令人困惑并且难以诊断出的错误.
拷贝构造函数是隐式调用的, 也就是说,这些调用很容易被忽略. 这会让人迷惑, 尤其是对那些所用的语言约定或强制要求传引用的程序员来说更是如此. 同时,这从一定程度上说会鼓励过度拷贝, 从而导致性能上的问题.
结论:
如果需要就让你的类型可拷贝 / 可移动.作为一个经验法则, 如果对于你的用户来说这个拷贝操作不是一眼就能看出来的, 那就不要把类型设置为可拷贝. 如果让类型可拷贝, 一定要同时给出拷贝构造函数和赋值操作的定义. 如果让类型可拷贝, 同时移动操作的效率高于拷贝操作, 那么就把移动的两个操作 (移动构造函数和赋值操作) 也给出定义.如果类型不可拷贝, 但是移动操作的正确性对用户显然可见, 那么把这个类型设置为只可移动并定义移动的两个操作.
建议通过 = default 定义拷贝和移动操作. 定义非默认的移动操作目前需要异常. 时刻记得检测默认操作的正确性. 由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个 public virtual Clone() 和一个 protected 的拷贝构造函数以供派生类实现.
如果你的类不需要拷贝 / 移动操作,请显式地通过 = delete 或其他手段禁用之.
3.5. 委派和继承构造函数
在能够减少重复代码的情况下使用委派和继承构造函数.
定义:
委派和继承构造函数是由 C++11 引进为了减少构造函数重复代码而开发的两种不同的特性. 通过特殊的初始化列表语法, 委派构造函数允许类的一个构造函数调用其他的构造函数.
继承构造函数允许派生类直接调用基类的构造函数, 一如继承基类的其他成员函数, 而无需重新声明. 当基类拥有多个构造函数时这一功能尤其有用.
如果派生类的构造函数只是调用基类的构造函数而没有其他行为时, 这一功能特别有用.
优点:
委派和继承构造函数可以减少冗余代码, 提高可读性.委派构造函数对 Java 程序员来说并不陌生.
缺点:
使用辅助函数可以预估出委派构造函数的行为. 如果派生类和基类相比引入了新的成员变量, 继承构造函数就会让人迷惑, 因为基类并不知道这些新的成员变量的存在.
结论:
只在能够减少冗余代码, 提高可读性的前提下使用委派和继承构造函数. 如果派生类有新的成员变量, 那么使用继承构造函数时要小心. 如果在派生类中对成员变量使用了类内部初始化的话, 继承构造函数还是适用的.
3.6结构体vs.类
仅当只有数据时使用 struct, 其它一概使用 class.
说明:
在 C++ 中 struct 和class 关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便未定义的数据类型选择合适的关键字.
struct 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域, 而非函数调用. 除了构造函数, 析构函数,Initialize(), Reset(), Validate() 等类似的函数外, 不能提供其它功能的函数.
如果需要更多的函数功能, class 更适合. 如果拿不准,就用 class.
为了和 STL 保持一致, 对于仿函数和 trait 特性可以不用 class 而是使用 struct.
注意: 类和结构体的成员变量使用不同的命名规则.
3.7. 继承
使用组合 (composition, YuleFox 注: 这一点也是GoF 在 <
定义:
当子类继承基类时, 子类包含了父基类所有数据及操作的定义. C++ 实践中, 继承主要用于两种场合: 实现继承 (implementation inheritance), 子类继承父类的实现代码; 接口继承(interface inheritance), 子类仅继承父类的方法名称.
优点:
实现继承通过原封不动的复用基类代码减少了代码量. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误. 从编程角度而言, 接口继承是用来强制类输出特定的 API. 在类没有实现 API 中某个必须的方法时, 编译器同样会发现并报告错误.
缺点:
对于实现继承, 由于子类的实现代码散布在父类和子类间之间, 要理解其实现变得更加困难. 子类不能重写父类的非虚函数, 当然也就不能修改其实现. 基类也可能定义了一些数据成员, 还要区分基类的实际布局.
结论:
所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 “是一个”(“is-a”,YuleFox 注: 其他 “has-a” 情况下请使用组合) 的情况下使用继承: 如果 Bar 的确 “是一种” Foo, Bar 才能继承 Foo.
必要的话, 析构函数声明为 virtual. 如果你的类有虚函数, 则析构函数也应该为虚函数. 注意 数据成员在任何情况下都必须是私有的.
当重载一个虚函数, 在衍生类中把它明确的声明为 virtual. 理论依据: 如果省略virtual 关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数.
3.8. 多重继承
真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的 纯接口类.
定义:
多重继承允许子类拥有多个基类. 要将作为 纯接口 的基类和具有 实现 的基类区别开来.
优点:
相比单继承 (见 继承), 多重实现继承可以复用更多的代码.
缺点:
真正需要用到多重 实现 继承的情况少之又少. 多重实现继承看上去是不错的解决方案, 但你通常也可以找到一个更明确, 更清晰的不同解决方案.
结论:
只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以 Interface 为后缀
3.9. 接口
接口是指满足特定条件的类, 这些类以Interface 为后缀 (不强制).
定义:
当一个类满足以下要求时, 称之为纯接口:
1.只有纯虚函数 (“=0”) 和静态函数(除了下文提到的析构函数).
2.没有非静态数据成员.
3.没有定义任何构造函数. 如果有,也不能带有参数, 并且必须为protected.
4.如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.
接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数 (作为上述第 1 条规则的特例, 析构函数不能是纯虚函数). 具体细节可参考 Stroustrup 的 The C++ Programming Language, 3rdedition 第 12.4 节.
优点:
以 Interface 为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员. 这一点对于 多重继承 尤其重要. 另外,对于 Java 程序员来说, 接口的概念已是深入人心.
缺点:
Interface后缀增加了类名长度, 为阅读和理解带来不便. 同时,接口特性作为实现细节不应暴露给用户.
结论:
只有在满足上述需要时, 类才以Interface 结尾, 但反过来,满足上述需要的类未必一定以 Interface 结尾.
3.10. 运算符重载
除少数特定环境外,不要重载运算符.
定义:
一个类可以定义诸如 + 和/ 等运算符, 使其可以像内建类型一样直接操作.
优点:
使代码看上去更加直观, 类表现的和内建类型 (如 int) 行为一致. 重载运算符使 Equals(), Add() 等函数名黯然失色. 为了使一些模板函数正确工作, 你可能必须定义操作符.
缺点:
虽然操作符重载令代码更加直观, 但也有一些不足:
1.混淆视听, 让你误以为一些耗时的操作和操作内建类型一样轻巧.
2.更难定位重载运算符的调用点, 查找Equals() 显然比对应的 == 调用点要容易的多.
3.有的运算符可以对指针进行操作, 容易导致bug. Foo + 4 做的是一件事, 而&Foo + 4 可能做的是完全不同的另一件事. 对于二者,编译器都不会报错, 使其很难调试;
重载还有令你吃惊的副作用. 比如,重载了 operator& 的类不能被前置声明.
结论:
一般不要重载运算符. 尤其是赋值操作 (operator=) 比较诡异, 应避免重载.如果需要的话, 可以定义类似 Equals(), CopyFrom() 等函数.
然而, 极少数情况下可能需要重载运算符以便与模板或 “标准” C++ 类互操作 (如operator<<(ostream&, const T&)). 只有被证明是完全合理的才能重载, 但你还是要尽可能避免这样做. 尤其是不要仅仅为了在 STL 容器中用作键值就重载 operator== 或 operator<; 相反,你应该在声明容器的时候, 创建相等判断和大小比较的仿函数类型.
有些 STL 算法确实需要重载 operator== 时,你可以这么做, 记得别忘了在文档中说明原因.
参考 拷贝构造函数 和 函数重载.
3.11. 存取控制
将 所有 数据成员声明为 private, 并根据需要提供相应的存取函数. 例如, 某个名为foo_ 的变量, 其取值函数是 foo(). 还可能需要一个赋值函数 set_foo().
特例是, 静态常量数据成员 (一般写做 kFoo) 不需要是私有成员.
一般在头文件中把存取函数定义成内联函数.
参考 继承 和 函数命名
3.11. 声明顺序
在类中使用特定的声明顺序: public: 在 private: 之前,成员函数在数据成员 (变量)前;
类的访问控制区段的声明顺序依次为: public:, protected:, private:. 如果某区段没内容, 可以不声明.
.cc 文件中函数的定义应尽可能和声明顺序一致.
不要在类定义中内联大型函数. 通常,只有那些没有特别意义或性能要求高, 并且是比较短小的函数才能被定义为内联函数. 更多细节参考 内联函数.
3.12. 编写简短函数
倾向编写简短, 凝练的函数.
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行,可以思索一下能不能在不影响程序结构的前提下对其进行分割.
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题. 甚至导致难以发现的 bug. 使函数尽量简短, 便于他人阅读和修改代码.
在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码: 如果证实这些代码使用 / 调试困难, 或者你需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数.
译者 (YuleFox) 笔记
1.不在构造函数中做太多逻辑相关的初始化;
2.编译器提供的默认构造函数不会对变量进行初始化, 如果定义了其他构造函数, 编译器不再提供, 需要编码者自行提供默认构造函数;
3.为避免隐式转换, 需将单参数构造函数声明为 explicit;
4.为避免拷贝构造函数, 赋值操作的滥用和编译器自动生成, 可将其声明为 private 且无需实现;
5.仅在作为数据集合时使用 struct;
6.组合 > 实现继承> 接口继承 > 私有继承, 子类重载的虚函数也要声明 virtual 关键字, 虽然编译器允许不这样做;
7.避免使用多重继承, 使用时,除一个基类含有实现外, 其他基类均为纯接口;
8.接口类类名以 Interface 为后缀,除提供带实现的虚析构函数, 静态成员函数外, 其他均为纯虚函数, 不定义非静态数据成员, 不提供构造函数, 提供的话,声明为 protected;
9.为降低复杂性, 尽量不重载操作符, 模板, 标准类中使用时提供文档说明;
10.存取函数一般内联在头文件中;
11.声明次序: public -> protected -> private;
12.函数体尽量短小, 紧凑,功能单一;
4. 来自 Google 的奇技
Google 用了很多自己实现的技巧 / 工具使C++ 代码更加健壮, 我们使用C++ 的方式可能和你在其它地方见到的有所不同.
4.1. 所有权与智能指针
动态分配出的对象最好有单一且固定的所有主(onwer), 且通过智能指针传递所有权(ownership).
定义:
所有权是一种登记/管理动态内存和其它资源的技术。动态分配出的对象的所有主是一个对象或函数,后者负责确保当前者无用时就自动销毁前者。所有权有时可以共享,那么就由最后一个所有主来负责销毁它。甚至也可以不用共享,在代码中直接把所有权传递给其它对象。
其实您可以把智能指针当成一个重载了 * 和-> 的「对象」来看。智能指针类型被用来自动化所有权的登记工作,来确保执行销毁义务到位。std::unique_ptr 是 C++11 新推出的一种智能指针类型,用来表示动态分配出的对象的「独一无二」所有权;当 std::unique_ptr 离开作用域,对象就会被销毁。不能复制 std::unique_ptr, 但可以把它移动(move)给新所有主。std::shared_ptr 同样表示动态分配对象的所有权,但可以被共享,也可以被复制;对象的所有权由所有复制者共同拥有,最后一个复制者被销毁时,对象也会随着被销毁。
优点:
1.如果没有清晰、逻辑条理的所有权安排,不可能管理好动态分配的内存。
2.传递对象的所有权,开销比复制来得小,如果可以复制的话。
3.传递所有权也比「借用」指针或引用来得简单,毕竟它大大省去了两个用户一起协调对象生命周期的工作。
4.如果所有权逻辑条理,有文档且不乱来的话,可读性很棒。
5.可以不用手动完成所有权的登记工作,大大简化了代码,也免去了一大波错误之恼。
6.对于 const 对象来说,智能指针简单易用,也比深度复制高效。
缺点:
1.不得不用指针(不管是智能的还是原生的)来表示和传递所有权。指针语义可要比值语义复杂得许多了,特别是在 API 里:您不光要操心所有权,还要顾及别名,生命周期,可变性(mutability)以及其它大大小小问题。
2.其实值语义的开销经常被高估,所以就所有权的性能来说,可不能光只考虑可读性以及复杂性。
3.如果 API 依赖所有权的传递,就会害得客户端不得不用单一的内存管理模型。
4.销毁资源并回收的相关代码不是很明朗。
5.std::unique_ptr的所有权传递原理是 C++11 的 move 语法,后者毕竟是刚刚推出的,容易迷惑程序员。
6.如果原本的所有权设计已经够完善了,那么若要引入所有权共享机制,可能不得不重构整个系统。
7.所有权共享机制的登记工作在运行时进行,开销可能相当不小。
8.某些极端情况下,所有权被共享的对象永远不会被销毁,比如引用死循环(cyclic references)。
9.智能指针并不能够完全代替原生指针。
决定:
如果必须使用动态分配,倾向于保持分配者的所有权。如果其他地方要使用这个对象,最好传递它的拷贝,或者传递一个不用改变所有权的指针或引用。倾向于使用 std::unique_ptr 来明确所有权传递
4.2. cpplint
使用 cpplint.py 检查风格错误.
cpplint.py是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报, 但它仍然是一个非常有用的工具. 在行尾加// NOLINT, 或在上一行加 // NOLINTNEXTLINE, 可以忽略报错。
某些项目会指导你如何使用他们的项目工具运行 cpplint.py. 如果你参与的项目没有提供, 你可以单独下载 cpplint.py.
译者(acgtyrant)笔记
1.把智能指针当成对象来看待的话,就很好领会它与所指对象之间的关系了。
2.原来 Rust 的Ownership 思想是受到了 C++ 智能指针的很大启发啊。
3.scoped_ptr和 auto_ptr 已过时。 现在是 shared_ptr 和 uniqued_ptr 的天下了。
4.按本文来说,似乎除了智能指针,还有其它所有权机制,值得留意。
5.ArchLinux 用户注意了,AUR 有对cpplint 打包。
5. 其他 C++ 特性
5.1. 引用参数
所有按引用传递的参数必须加上 const.
定义:
在 C 语言中,如果函数需要修改变量的值, 参数必须为指针, 如 int foo(int *pval). 在 C++ 中,函数还可以声明引用参数: int foo(int &val).
优点:
定义引用参数防止出现 (*pval)++ 这样丑陋的代码. 像拷贝构造函数这样的应用也是必需的. 而且更明确, 不接受NULL 指针.
缺点:
容易引起误解, 因为引用在语法上是值变量却拥有指针的语义.
结论:
函数参数列表中, 所有引用参数都必须是 const:
voidFoo(const string &in, string *out);
事实上这在 Google Code 是一个硬性约定: 输入参数是值参或 const 引用, 输出参数为指针. 输入参数可以是 const 指针, 但决不能是非 const 的引用参数,除非用于交换,比如 swap().
有时候,在输入形参中用 const T* 指针比 const T& 更明智。比如:
1.您会传 null 指针。
2.函数要把指针或对地址的引用赋值给输入形参。
总之大多时候输入形参往往是 const T&. 若用 const T* 说明输入另有处理。所以若您要用 const T*, 则应有理有据,否则会害得读者误解。
5.2. 右值引用
只在定义移动构造函数与移动赋值操作时使用右值引用. 不要使用std::forward.
定义:
右值引用是一种只能绑定到临时对象的引用的一种, 其语法与传统的引用语法相似. 例如, void f(string&& s); 声明了一个其参数是一个字符串的右值引用的函数.
优点:
用于定义移动构造函数 (使用类的右值引用进行构造的函数) 使得移动一个值而非拷贝之成为可能. 例如,如果 v1 是一个vector
右值引用使得编写通用的函数封装来转发其参数到另外一个函数成为可能, 无论其参数是否是临时对象都能正常工作.
右值引用能实现可移动但不可拷贝的类型, 这一特性对那些在拷贝方面没有实际需求, 但有时又需要将它们作为函数参数传递或塞入容器的类型很有用.
要高效率地使用某些标准库类型, 例如std::unique_ptr, std::move 是必需的.
缺点:
右值引用是一个相对比较新的特性 (由C++11 引入), 它尚未被广泛理解. 类似引用崩溃, 移动构造函数的自动推导这样的规则都是很复杂的.
结论:
只在定义移动构造函数与移动赋值操作时使用右值引用, 不要使用std::forward 功能函数. 你可能会使用 std::move 来表示将值从一个对象移动而不是复制到另一个对象.
5.3. 函数重载
若要用好函数重载,最好能让读者一看调用点(call site)就胸有成竹,不用花心思猜测调用的重载函数到底是哪一种。该规则适用于构造函数。
定义:
你可以编写一个参数类型为 const string& 的函数, 然后用另一个参数类型为 const char* 的函数重载它:
优点:
通过重载参数不同的同名函数, 令代码更加直观. 模板化代码需要重载, 同时为使用者带来便利.
缺点:
如果函数单单靠不同的参数类型而重载(acgtyrant 注:这意味着参数数量不变),读者就得十分熟悉 C++ 五花八门的匹配规则,以了解匹配过程具体到底如何。另外,当派生类只重载了某个函数的部分变体,继承语义容易令人困惑。
结论:
如果您打算重载一个函数, 可以试试改在函数名里加上参数信息。例如,用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append().
5.4. 缺省参数
我们不允许使用缺省函数参数,少数极端情况除外。尽可能改用函数重载。
优点:
当您有依赖缺省参数的函数时,您也许偶尔会修改修改这些缺省参数。通过缺省参数,不用再为个别情况而特意定义一大堆函数了。与函数重载相比,缺省参数语法更为清晰,代码少,也很好地区分了「必选参数」和「可选参数」。
缺点:
缺省参数会干扰函数指针,害得后者的函数签名(function signature)往往对不上所实际要调用的函数签名。即在一个现有函数添加缺省参数,就会改变它的类型,那么调用其地址的代码可能会出错,不过函数重载就没这问题了。此外,缺省参数会造成臃肿的代码,毕竟它们在每一个调用点(call site)都有重复(acgtyrant 注:我猜可能是因为调用函数的代码表面上看来省去了不少参数,但编译器在编译时还是会在每一个调用代码里统统补上所有默认实参信息,造成大量的重复)。函数重载正好相反,毕竟它们所谓的「缺省参数」只会出现在函数定义里。
结论:
由于缺点并不是很严重,有些人依旧偏爱缺省参数胜于函数重载。所以除了以下情况,我们要求必须显式提供所有参数(acgtyrant 注:即不能再通过缺省参数来省略参数了)。
其一,位于 .cc 文件里的静态函数或匿名空间函数,毕竟都只能在局部文件里调用该函数了。
其二,可以在构造函数里用缺省参数,毕竟不可能取得它们的地址。
其三,可以用来模拟变长数组。
5.5. 变长数组和 alloca()
我们不允许使用变长数组和 alloca().
优点:
变长数组具有浑然天成的语法. 变长数组和alloca() 也都很高效.
缺点:
变长数组和 alloca() 不是标准 C++ 的组成部分.更重要的是, 它们根据数据大小动态分配堆栈内存, 会引起难以发现的内存越界 bugs: “在我的机器上运行的好好的, 发布后却莫名其妙的挂掉了”.
结论:
改用更安全的分配器(allocator),就像 std::vector 或std::unique_ptr
5.6. 友元
我们允许合理的使用友元类及友元函数.
通常友元应该定义在同一文件内, 避免代码读者跑到其它文件查找使用该私有成员的类. 经常用到友元的一个地方是将 FooBuilder 声明为 Foo 的友元, 以便FooBuilder 正确构造 Foo 的内部状态, 而无需将该状态暴露出来. 某些情况下, 将一个单元测试类声明成待测类的友元会很方便.
友元扩大了 (但没有打破)类的封装边界. 某些情况下,相对于将类成员声明为 public, 使用友元是更好的选择, 尤其是如果你只允许另一个类访问该类的私有成员时. 当然,大多数类都只应该通过其提供的公有成员进行互操作.
5.7. 异常
我们不使用 C++ 异常.
优点:
1.异常允许应用高层决定如何处理在底层嵌套函数中「不可能发生」的失败(failures),不用管那些含糊且容易出错的错误代码(acgtyrant 注:error code, 我猜是C语言函数返回的非零 int 值)。
2.很多现代语言都用异常。引入异常使得 C++ 与 Python, Java 以及其它类C++ 的语言更一脉相承。
3.有些第三方 C++ 库依赖异常,禁用异常就不好用了。
4.异常是处理构造函数失败的唯一途径。虽然可以用工厂函数(acgtyrant 注:factory function, 出自 C++ 的一种设计模式,即「简单工厂模式」)或 Init() 方法代替异常, 但是前者要求在堆栈分配内存,后者会导致刚创建的实例处于 ”无效“ 状态。
5.在测试框架里很好用。
缺点:
1.在现有函数中添加 throw 语句时,您必须检查所有调用点。要么让所有调用点统统具备最低限度的异常安全保证,要么眼睁睁地看异常一路欢快地往上跑,最终中断掉整个程序。举例,f() 调用g(), g() 又调用 h(), 且 h 抛出的异常被 f 捕获。当心 g, 否则会没妥善清理好。
2.还有更常见的,异常会彻底扰乱程序的执行流程并难以判断,函数也许会在您意料不到的地方返回。您或许会加一大堆何时何处处理异常的规定来降低风险,然而开发者的记忆负担更重了。
3.异常安全需要RAII和不同的编码实践. 要轻松编写出正确的异常安全代码需要大量的支持机制. 更进一步地说, 为了避免读者理解整个调用表, 异常安全必须隔绝从持续状态写到“提交” 状态的逻辑.这一点有利有弊 (因为你也许不得不为了隔离提交而混淆代码). 如果允许使用异常, 我们就不得不时刻关注这样的弊端, 即使有时它们并不值得.
4.启用异常会增加二进制文件数据,延长编译时间(或许影响小),还可能加大地址空间的压力。
5.滥用异常会变相鼓励开发者去捕捉不合时宜,或本来就已经没法恢复的「伪异常」。比如,用户的输入不符合格式要求时,也用不着抛异常。如此之类的伪异常列都列不完。
结论:
从表面上看来,使用异常利大于弊, 尤其是在新项目中. 但是对于现有代码, 引入异常会牵连到所有相关代码. 如果新项目允许异常向外扩散, 在跟以前未使用异常的代码整合时也将是个麻烦. 因为Google 现有的大多数 C++ 代码都没有异常处理, 引入带有异常处理的新代码相当困难.
鉴于 Google 现有代码不接受异常, 在现有代码中使用异常比在新项目中使用的代价多少要大一些. 迁移过程比较慢, 也容易出错. 我们不相信异常的使用有效替代方案, 如错误代码, 断言等会造成严重负担.
我们并不是基于哲学或道德层面反对使用异常, 而是在实践的基础上. 我们希望在 Google 使用我们自己的开源项目, 但项目中使用异常会为此带来不便, 因此我们也建议不要在 Google 的开源项目中使用异常. 如果我们需要把这些项目推倒重来显然不太现实.
对于 Windows 代码来说, 有个特例.
(YuleFox 注: 对于异常处理, 显然不是短短几句话能够说清楚的, 以构造函数为例, 很多 C++ 书籍上都提到当构造失败时只有异常可以处理, Google 禁止使用异常这一点, 仅仅是为了自身的方便, 说大了, 无非是基于软件管理成本上, 实际使用中还是自己决定)
5.8. 运行时类型识别TODO
我们禁止使用 RTTI.
定义:
RTTI 允许程序员在运行时识别 C++ 类对象的类型. 它通过使用 typeid 或者 dynamic_cast 完成.
优点:
RTTI 的标准替代 (下面将描述)需要对有问题的类层级进行修改或重构. 有时这样的修改并不是我们所想要的, 甚至是不可取的, 尤其是在一个已经广泛使用的或者成熟的代码中.
RTTI 在某些单元测试中非常有用. 比如进行工厂类测试时, 用来验证一个新建对象是否为期望的动态类型. RTTI 对于管理对象和派生对象的关系也很有用.
在考虑多个抽象对象时 RTTI 也很好用
缺点:
在运行时判断类型通常意味着设计问题. 如果你需要在运行期间确定一个对象的类型, 这通常说明你需要考虑重新设计你的类.
随意地使用 RTTI 会使你的代码难以维护. 它使得基于类型的判断树或者 switch 语句散布在代码各处. 如果以后要进行修改, 你就必须检查它们.
结论:
RTTI 有合理的用途但是容易被滥用, 因此在使用时请务必注意. 在单元测试中可以使用 RTTI, 但是在其他代码中请尽量避免. 尤其是在新代码中, 使用 RTTI 前务必三思. 如果你的代码需要根据不同的对象类型执行不同的行为的话, 请考虑用以下的两种替代方案之一查询类型:
虚函数可以根据子类类型的不同而执行不同代码. 这是把工作交给了对象本身去处理.
如果这一工作需要在对象之外完成, 可以考虑使用双重分发的方案, 例如使用访问者设计模式. 这就能够在对象之外进行类型判断.
如果程序能够保证给定的基类实例实际上都是某个派生类的实例, 那么就可以自由使用 dynamic_cast. 在这种情况下, 使用 dynamic_cast 也是一种替代方案.
基于类型的判断树是一个很强的暗示, 它说明你的代码已经偏离正轨了
不要去手工实现一个类似 RTTI 的方案. 反对RTTI 的理由同样适用于这些方案, 比如带类型标签的类继承体系. 而且, 这些方案会掩盖你的真实意图.
5.9. 类型转换
使用 C++ 的类型转换, 如static_cast<>(). 不要使用int y = (int)x 或 int y = int(x) 等转换方式;
定义:
C++ 采用了有别于 C 的类型转换机制, 对转换操作进行归类.
优点:
C 语言的类型转换问题在于模棱两可的操作; 有时是在做强制转换 (如 (int)3.5), 有时是在做类型转换 (如(int)"hello"). 另外,C++ 的类型转换在查找时更醒目.
缺点:
恶心的语法.
结论:
不要使用 C 风格类型转换. 而应该使用 C++ 风格.
1.用 static_cast 替代C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时.
2.用 const_cast 去掉const 限定符.
3.用 reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换. 仅在你对所做一切了然于心时使用.
至于 dynamic_cast 参见 5.8. 运行时类型识别.
5.10. 流
只在记录日志时使用流.
定义:
流用来替代 printf() 和 scanf().
优点:
有了流, 在打印时不需要关心对象的类型. 不用担心格式化字符串与参数列表不匹配 (虽然在gcc 中使用 printf 也不存在这个问题). 流的构造和析构函数会自动打开和关闭对应的文件.
缺点:
流使得 pread() 等功能函数很难执行. 如果不使用printf 风格的格式化字符串, 某些格式化操作 (尤其是常用的格式字符串 %.*s) 用流处理性能是很低的. 流不支持字符串操作符重新排序 (%1s), 而这一点对于软件国际化很有用.
结论:
不要使用流, 除非是日志接口需要. 使用 printf 之类的代替.
使用流还有很多利弊, 但代码一致性胜过一切. 不要在代码中使用流.
拓展讨论:
对这一条规则存在一些争论, 这儿给出点深层次原因. 回想一下唯一性原则 (Only One Way): 我们希望在任何时候都只使用一种确定的 I/O 类型, 使代码在所有 I/O 处都保持一致. 因此,我们不希望用户来决定是使用流还是 printf + read/write. 相反, 我们应该决定到底用哪一种方式. 把日志作为特例是因为日志是一个非常独特的应用, 还有一些是历史原因.
流的支持者们主张流是不二之选, 但观点并不是那么清晰有力. 他们指出的流的每个优势也都是其劣势. 流最大的优势是在输出时不需要关心打印对象的类型. 这是一个亮点. 同时, 也是一个不足: 你很容易用错类型, 而编译器不会报警. 使用流时容易造成的这类错误:
cout<< this; // 输出地址
cout<< *this; // 输出值
由于 << 被重载, 编译器不会报错. 就因为这一点我们反对使用操作符重载.
有人说 printf 的格式化丑陋不堪, 易读性差,但流也好不到哪儿去.
5.11. 前置自增和自减
对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.
定义:
对于变量在自增 (++i 或 i++) 或自减(--i 或 i--) 后表达式的值又没有没用到的情况下, 需要确定到底是使用前置还是后置的自增 (自减).
优点:
不考虑返回值的话, 前置自增(++i) 通常要比后置自增 (i++) 效率更高. 因为后置自增 (或自减) 需要对表达式的值 i 进行一次拷贝. 如果i 是迭代器或其他非数值类型, 拷贝的代价是比较大的. 既然两种自增方式实现的功能一样, 为什么不总是使用前置自增呢?
缺点:
在 C 开发中,当表达式的值未被使用时, 传统的做法是使用后置自增, 特别是在 for 循环中. 有些人觉得后置自增更加易懂, 因为这很像自然语言, 主语(i) 在谓语动词 (++) 前.
结论:
对简单数值 (非对象),两种都无所谓. 对迭代器和模板类型, 使用前置自增 (自减).
5.12. const 用法
我们强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的constexpr 更好。
定义:
在声明的变量或参数前加上关键字 const 用于指明变量值不可被篡改 (如const int foo ). 为类中的函数加上 const 限定符表明该函数不会修改类成员变量的状态 (如class Foo { int Bar(char c) const; };).
优点:
大家更容易理解如何使用变量. 编译器可以更好地进行类型检测, 相应地, 也能生成更好的代码. 人们对编写正确的代码更加自信, 因为他们知道所调用的函数被限定了能或不能修改变量值. 即使是在无锁的多线程编程中, 人们也知道什么样的函数是安全的.
缺点:
const 是入侵性的: 如果你向一个函数传入 const 变量, 函数原型声明中也必须对应 const 参数 (否则变量需要 const_cast 类型转换), 在调用库函数时显得尤其麻烦.
结论:
const 变量, 数据成员,函数和参数为编译时类型检测增加了一层保障; 便于尽早发现错误. 因此, 我们强烈建议在任何可能的情况下使用 const:
1.如果函数不会修改传你入的引用或指针类型参数, 该参数应声明为 const.
2.尽可能将函数声明为 const. 访问函数应该总是 const. 其他不会修改任何数据成员, 未调用非const 函数, 不会返回数据成员非 const 指针或引用的函数也应该声明成 const.
3.如果数据成员在对象构造之后不再发生变化, 可将其定义为 const.
然而, 也不要发了疯似的使用 const. 像 const int * const * const x; 就有些过了, 虽然它非常精确的描述了常量 x. 关注真正有帮助意义的信息: 前面的例子写成 const int** x 就够了.
关键字 mutable 可以使用, 但是在多线程中是不安全的, 使用时首先要考虑线程安全.
const 的位置:
有人喜欢 int const *foo 形式, 不喜欢const int* foo, 他们认为前者更一致因此可读性也更好: 遵循了const 总位于其描述的对象之后的原则. 但是一致性原则不适用于此, “不要过度使用” 的声明可以取消大部分你原本想保持的一致性. 将const 放在前面才更易读, 因为在自然语言中形容词 (const) 是在名词 (int) 之前.
这是说, 我们提倡但不强制 const 在前. 但要保持代码的一致性! (Yang.Y 注: 也就是不要在一些地方把 const 写在类型前面, 在其他地方又写在后面, 确定一种写法, 然后保持一致.)
5.13. constexpr 用法
在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化。
定义:
变量可以被声明成 constexpr 以表示它是真正意义上的常量,即在编译时和运行时都不变。函数或构造函数也可以被声明成 constexpr, 以用来定义 constexpr 变量。
优点:
如今 constexpr 就可以定义浮点式的真?常量,不用再依赖字面值了;也可以定义用户自定义类型上的常量;甚至也可以定义函数调用所返回的常量。
缺点:
若过早把变量优化成 constexpr 变量,将来又要把它改为常规变量时,挺麻烦的;当前对constexpr函数和构造函数中允许的限制可能会导致这些定义中解决的方法模糊。
结论:
靠 constexpr 特性,方才实现了 C++ 在接口上打造真正常量机制的可能。好好用 constexpr 来定义真?常量以及支持常量的函数。避免复杂的函数定义,以使其能够与constexpr一起使用。 千万别痴心妄想地想靠 constexpr 来强制代码「内联」。
5.14. 整型
C++ 内建整型中, 仅使用int. 如果程序中需要不同大小的变量, 可以使用
定义:
C++ 没有指定整型的大小. 通常人们假定 short 是 16 位,int 是 32 位,long 是 32 位,long long 是 64 位.
优点:
保持声明统一.
缺点:
C++ 中整型大小因编译器和体系结构的不同而不同.
结论:
如果已知整数不会太大, 我们常常会使用 int, 如循环计数. 在类似的情况下使用原生类型 int. 你可以认为 int 至少为 32 位,但不要认为它会多于 32 位.如果需要 64 位整型,用 int64_t 或 uint64_t.对于大整数,使用 int64_t.不要使用 uint32_t 等无符号整型, 除非你是在表示一个位组而不是一个数值, 或是你需要定义二进制补码溢出. 尤其是不要为了指出数值永不会为负, 而使用无符号类型. 相反, 你应该使用断言来保护数据.
如果您的代码涉及容器返回的大小(size),确保其类型足以应付容器各种可能的用法。拿不准时,类型越大越好。
小心整型类型转换和整型提升(acgtyrant 注:integer promotions, 比如 int 与unsigned int 运算时,前者被提升为 unsigned int 而有可能溢出),总有意想不到的后果。
关于无符号整数:
有些人, 包括一些教科书作者, 推荐使用无符号类型表示非负数. 这种做法试图达到自我文档化. 但是, 在C 语言中, 这一优点被由其导致的 bug 所淹没. 看看下面的例子:
for(unsigned int i = foo.Length()-1; i >= 0; --i) ...
上述循环永远不会退出! 有时gcc 会发现该 bug 并报警, 但大部分情况下都不会. 类似的 bug 还会出现在比较有符合变量和无符号变量时. 主要是C 的类型提升机制会致使无符号类型的行为出乎你的意料.
因此, 使用断言来指出变量为非负数, 而不是使用无符号型!
5.15.位下的可移植性
代码应该对 64 位和32 位系统友好. 处理打印,比较, 结构体对齐时应切记:
1.对于某些类型, printf() 的指示符在32 位和 64 位系统上可移植性不是很好. C99 标准定义了一些可移植的格式化指示符. 不幸的是,MSVC 7.1 并非全部支持, 而且标准中也有所遗漏, 所以有时我们不得不自己定义一个丑陋的版本
1.记住 sizeof(void *) != sizeof(int). 如果需要一个指针大小的整数要用 intptr_t.
2.你要非常小心的对待结构体对齐, 尤其是要持久化到磁盘上的结构体 (Yang.Y 注: 持久化- 将数据按字节流顺序保存在磁盘文件或数据库中). 在64 位系统中, 任何含有int64_t/uint64_t 成员的类/结构体,缺省都以 8 字节在结尾对齐. 如果 32 位和64 位代码要共用持久化的结构体, 需要确保两种体系结构下的结构体对齐一致. 大多数编译器都允许调整结构体对齐. gcc 中可使用 __attribute__((packed)). MSVC 则提供了 #pragma pack() 和 __declspec(align()) (YuleFox 注, 解决方案的项目属性里也可以直接设置).
3.创建 64 位常量时使用 LL 或 ULL 作为后缀,
4.如果你确实需要 32 位和64 位系统具有不同代码, 可以使用#ifdef _LP64 指令来切分 32/64 位代码. (尽量不要这么做, 如果非用不可, 尽量使修改局部化)
5.16. 预处理宏
使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.
宏意味着你和编译器看到的代码是不同的. 这可能会导致异常行为, 尤其因为宏具有全局作用域.
值得庆幸的是, C++ 中, 宏不像在C 中那么必不可少. 以往用宏展开性能关键的代码, 现在可以用内联函数替代. 用宏表示常量可被 const 变量代替. 用宏 “缩写” 长变量名可被引用代替. 用宏进行条件编译... 这个, 千万别这么做, 会令测试更加痛苦 (#define 防止头文件重包含当然是个特例).
宏可以做一些其他技术无法实现的事情, 在一些代码库 (尤其是底层库中) 可以看到宏的某些特性 (如用 # 字符串化,用 ## 连接等等).但在使用前, 仔细考虑一下能不能不使用宏达到同样的目的.
下面给出的用法模式可以避免使用宏带来的问题; 如果你要宏,尽可能遵守:
1.不要在 .h 文件中定义宏.
2.在马上要使用时才进行 #define, 使用后要立即 #undef.
3.不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
4.不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.
5.不要用 ## 处理函数,类和变量的名字。
5.17. 0, nullptr 和 NULL
整数用 0, 实数用0.0, 指针用 nullptr 或 NULL, 字符(串) 用'\0'.
整数用 0, 实数用0.0, 这一点是毫无争议的.
对于指针 (地址值),到底是用 0, NULL 还是 nullptr. C++11 项目用 nullptr; C++03 项目则用 NULL, 毕竟它看起来像指针。实际上,一些 C++ 编译器对 NULL 的定义比较特殊,可以输出有用的警告,特别是 sizeof(NULL) 就和 sizeof(0) 不一样。
字符 (串)用 '\0', 不仅类型正确而且可读性好.
5.18. sizeof
尽可能用 sizeof(varname) 代替 sizeof(type).
使用 sizeof(varname) 是因为当代码中变量类型改变时会自动更新. 您或许会用sizeof(type) 处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式,这时用变量就不合适了。
5.19. auto
用 auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方。
定义:
C++11 中,若变量被声明成 auto, 那它的类型就会被自动匹配成初始化表达式的类型。您可以用 auto 来复制初始化或绑定引用。
优点:
C++ 类型名有时又长又臭,特别是涉及模板或命名空间的时候。就像:
sparse_hash_map
返回类型好难读,代码目的也不够一目了然。重构其:
auto iter= m.find(val);
好多了。
没有 auto 的话,我们不得不在同一个表达式里写同一个类型名两次,无谓的重复,就像:
diagnostics::ErrorStatus*status = new diagnostics::ErrorStatus("xyz");
有了 auto, 可以更方便地用中间变量,显式编写它们的类型轻松点。
缺点:
类型够明显时,特别是初始化变量时,代码才会够一目了然。但以下就不一样了:
auto i =x.Lookup(key);
看不出其类型是啥,x 的类型声明恐怕远在几百行之外了。
程序员必须会区分 auto 和 const auto& 的不同之处,否则会复制错东西。
auto 和 C++11 列表初始化的合体令人摸不着头脑:
autox(3); // 圆括号。
autoy{3}; // 大括号。
它们不是同一回事——x 是int, y 则是 std::initializer_list
如果在接口里用 auto, 比如声明头文件里的一个常量,那么只要仅仅因为程序员一时修改其值而导致类型变化的话——API 要翻天覆地了。
结论:
auto 只能用在局部变量里用。别用在文件作用域变量,命名空间作用域变量和类数据成员里。永远别列表初始化 auto 变量。
auto 还可以和 C++11 特性「尾置返回类型(trailing return type)」一起用,不过后者只能用在 lambda 表达式里。
5.20. 列表初始化
你可以用列表初始化。
早在 C++03 里,聚合类型(aggregate types)就已经可以被列表初始化了,比如数组和不自带构造函数的结构体:
structPoint { int x; int y; };
Point p ={1, 2};
C++11 中,该特性得到进一步的推广,任何对象类型都可以被列表初始化。
5.21. Lambda 表达式
适当使用 lambda 表达式。别用默认 lambda 捕获,所有捕获都要显式写出来。
定义:
Lambda 表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传
C++11 首次提出 Lambdas, 还提供了一系列处理函数对象的工具,比如多态包装器(polymorphic wrapper) std::function.
优点:
1.传函数对象给 STL 算法,Lambdas最简易,可读性也好。
2.Lambdas,std::functions 和 std::bind 可以搭配成通用回调机制(general purpose callback mechanism);写接收有界函数为参数的函数也很容易了。
缺点:
1.Lambdas的变量捕获略旁门左道,可能会造成悬空指针。
2.Lambdas可能会失控;层层嵌套的匿名函数难以阅读。
结论:
1.按 format 小用lambda 表达式怡情。
2.禁用默认捕获,捕获都要显式写出来。打比方,比起 [=](int x) {return x + n;}, 您该写成 [n](int x) {return x + n;} 才对,这样读者也好一眼看出 n 是被捕获的值。
3.匿名函数始终要简短,如果函数体超过了五行,那么还不如起名(acgtyrant 注:即把 lambda 表达式赋值给对象),或改用函数。
4.如果可读性更好,就显式写出 lambd 的尾置返回类型,就像auto.
5.22. 模板编程
不要使用复杂的模板编程
定义:
模板编程指的是利用c++ 模板实例化机制是图灵完备性, 可以被用来实现编译时刻的类型判断的一系列编程技巧
优点:
模板编程能够实现非常灵活的类型安全的接口和极好的性能, 一些常见的工具比如Google Test, std::tuple, std::function 和Boost.Spirit. 这些工具如果没有模板是实现不了的
缺点:
1.模板编程所使用的技巧对于使用c++不是很熟练的人是比较晦涩, 难懂的. 在复杂的地方使用模板的代码让人更不容易读懂, 并且debug和 维护起来都很麻烦
2.模板编程经常会导致编译出错的信息非常不友好: 在代码出错的时候, 即使这个接口非常的简单, 模板内部复杂的实现细节也会在出错信息显示. 导致这个编译出错信息看起来非常难以理解.
3.大量的使用模板编程接口会让重构工具(Visual Assist X, Refactor for C++等等)更难发挥用途. 首先模板的代码会在很多上下文里面扩展开来, 所以很难确认重构对所有的这些展开的代码有用, 其次有些重构工具只对已经做过模板类型替换的代码的AST 有用.因此重构工具对这些模板实现的原始代码并不有效, 很难找出哪些需要重构.
结论:
1.模板编程有时候能够实现更简洁更易用的接口, 但是更多的时候却适得其反. 因此模板编程最好只用在少量的基础组件, 基础数据结构上, 因为模板带来的额外的维护成本会被大量的使用给分担掉
2.在使用模板编程或者其他复杂的模板技巧的时候, 你一定要再三考虑一下. 考虑一下你们团队成员的平均水平是否能够读懂并且能够维护你写的模板代码.或者一个非c++程序员和一些只是在出错的时候偶尔看一下代码的人能够读懂这些错误信息或者能够跟踪函数的调用流程. 如果你使用递归的模板实例化, 或者类型列表, 或者元函数,又或者表达式模板, 或者依赖SFINAE,或者sizeof 的trick 手段来检查函数是否重载, 那么这说明你模板用的太多了, 这些模板太复杂了, 我们不推荐使用
3.如果你使用模板编程, 你必须考虑尽可能的把复杂度最小化, 并且尽量不要让模板对外暴漏. 你最好只在实现里面使用模板, 然后给用户暴露的接口里面并不使用模板, 这样能提高你的接口的可读性. 并且你应该在这些使用模板的代码上写尽可能详细的注释. 你的注释里面应该详细的包含这些代码是怎么用的, 这些模板生成出来的代码大概是什么样子的. 还需要额外注意在用户错误使用你的模板代码的时候需要输出更人性化的出错信息. 因为这些出错信息也是你的接口的一部分, 所以你的代码必须调整到这些错误信息在用户看起来应该是非常容易理解, 并且用户很容易知道如何修改这些错误
5.23. Boost 库
只使用 Boost 中被认可的库.
定义:
Boost 库集 是一个广受欢迎, 经过同行鉴定, 免费开源的 C++ 库集.
优点:
Boost代码质量普遍较高, 可移植性好,填补了 C++ 标准库很多空白, 如型别的特性, 更完善的绑定器, 更好的智能指针。
缺点:
某些 Boost 库提倡的编程实践可读性差, 比如元编程和其他高级模板技术, 以及过度 “函数化” 的编程风格.
结论:
为了向阅读和维护代码的人员提供更好的可读性, 我们只允许使用 Boost 一部分经认可的特性子集. 目前允许使用以下库:
1.CallTraits : boost/call_traits.hpp
2.CompressedPair : boost/compressed_pair.hpp
3. 4.PropertyMap : boost/property_map.hpp 5.Thepart of Iterator that deals with defining iterators:boost/iterator/iterator_adaptor.hpp, boost/iterator/iterator_facade.hpp, andboost/function_output_iterator.hpp 6.Thepart of Polygon that deals with Voronoi diagram construction and doesn’t dependon the rest of Polygon: boost/polygon/voronoi_builder.hpp,boost/polygon/voronoi_diagram.hpp, and boost/polygon/voronoi_geometry_type.hpp 7.Bimap :boost/bimap 8.StatisticalDistributions and Functions : boost/math/distributions 9.Multi-index: boost/multi_index 10.Heap :boost/heap 11.heflat containers from Container: boost/container/flat_map, andboost/container/flat_set 我们正在积极考虑增加其它 Boost 特性, 所以列表中的规则将不断变化. 以下库可以用,但由于如今已经被 C++ 11 标准库取代,不再鼓励: 1.PointerContainer : boost/ptr_container, 改用std::unique_ptr 2.Array :boost/array.hpp, 改用 std::array 5.24. C++11 适当用 C++11(前身是 C++0x)的库和语言扩展,在贵项目用 C++11 特性前三思可移植性。 定义: C++11 有众多语言和库上的`变革 优点: 在二〇一四年八月之前,C++11 一度是官方标准,被大多 C++ 编译器支持。它标准化很多我们早先就在用的 C++ 扩展,简化了不少操作,大大改善了性能和安全。 缺点: C++11 相对于前身,复杂极了:1300 页vs 800 页!很多开发者也不怎么熟悉它。于是从长远来看,前者特性对代码可读性以及维护代价难以预估。我们说不准什么时候采纳其特性,特别是在被迫依赖老实工具的项目上。 和 5.23. Boost 库 一样,有些 C++11 扩展提倡实则对可读性有害的编程实践——就像去除冗余检查(比如类型名)以帮助读者,或是鼓励模板元编程等等。有些扩展在功能上与原有机制冲突,容易招致困惑以及迁移代价。 缺点: C++11 特性除了个别情况下,可以用一用。除了本指南会有不少章节会加以讨若干 C++11 特性之外,以下特性最好不要用: 1.尾置返回类型,比如用 auto foo() -> int 代替 int foo(). 为了兼容于现有代码的声明风格。 2.编译时合数 3. 4.默认 lambda 捕获。 6. 命名约定 最重要的一致性规则是命名管理. 命名风格快速获知名字代表是什么东东: 类型? 变量?函数? 常量?宏 ... ? 甚至不需要去查找类型声明. 我们大脑中的模式匹配引擎可以非常可靠的处理这些命名规则. 命名规则具有一定随意性, 但相比按个人喜好命名, 一致性更重要, 所以不管你怎么想, 规则总归是规则. 6.1. 通用命名规则 函数命名,变量命名,文件命名要有描述性;少用缩写。 尽可能给有描述性的命名,别心疼空间,毕竟让代码易于新读者理解很重要。不要用只有项目开发者能理解的缩写,也不要通过砍掉几个字母来缩写单词。 intprice_count_reader; // 无缩写 intnum_errors; // “num” 本来就很常见 intnum_dns_connections; // 人人都知道 “DNS” 是啥 ?Warning intn; // 莫名其妙。 intnerr; // 怪缩写。 intn_comp_conns; // 怪缩写。 intwgc_connections; // 只有贵团队知道是啥意思。 intpc_reader; // "pc" 有太多可能的解释了。 intcstmr_id; // 有删减若干字母。 6.2文件命名 文件名要全部小写, 可以包含下划线 (_) 或连字符 (-). 按项目约定来. 如果并没有项目约定,”_” 更好。 可接受的文件命名: *my_useful_class.cc *my-useful-class.cc *myusefulclass.cc *muusefulclass_test.cc // ``_unittest`` 和``_regtest`` 已弃用。 C++ 文件要以 .cc 结尾,头文件以 .h 结尾.专门插入文本的文件则以 .inc 结尾,参见 1.1. Self-contained 头文件。 不要使用已经存在于 /usr/include 下的文件名 (Yang.Y 注:即编译器搜索系统头文件的路径), 如db.h. 通常应尽量让文件名更加明确. http_server_logs.h 就比 logs.h 要好.定义类时文件名一般成对出现, 如foo_bar.h 和 foo_bar.cc, 对应于类 FooBar. 内联函数必须放在 .h 文件中.如果内联函数比较短, 就直接放在.h 中. 6.3. 类型命名 类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum. 所有类型命名 —— 类, 结构体,类型定义 (typedef), 枚举 —— 均使用相同约定. 6.4. 变量命名 变量名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用,如:: a_local_variable, a_struct_data_member, a_class_data_member_. 普通变量命名: 类数据成员: 不管是静态的还是非静态的,类数据成员都可以和普通变量一样, 但要接下划线。 结构体变量: 不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样, 不用像类那样接下划线 结构体与类的讨论参考 结构体vs. 类 一节. 全局变量: 对全局变量没有特别要求, 少用就好,但如果你要用, 可以用g_ 或其它标志作为前缀, 以便更好的区分局部变量. 6.5. 常量命名 在全局或类里的常量名称前加 k: kDaysInAWeek. 且除去开头的 k 之外每个单词开头字母均大写。 所有编译时常量, 无论是局部的, 全局的还是类中的, 和其他变量稍微区别一下. k 后接大写字母开头的单词: const int kDaysInAWeek = 7; 这规则适用于编译时的局部作用域常量,不过要按变量规则来命名也可以。 6.6. 函数命名 常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction(), MyExcitingMethod(),my_exciting_member_variable(), set_my_exciting_member_variable(). 常规函数: 函数名的每个单词首字母大写, 没有下划线。 如果您的某函数出错时就要直接 crash, 那么就在函数名加上 OrDie. 但这函数本身必须集成在产品代码里,且平时也可能会出错。 取值和设值函数: 取值(Accessors)和设值(Mutators)函数要与存取的变量名匹配. 这儿摘录一个类, num_entries_ 是该类的实例变量: 其它非常短小的内联函数名也可以用小写字母, 例如.如果你在循环中调用这样的函数甚至都不用缓存其返回值, 小写命名就可以接受. 6.7. 名字空间命名 名字空间用小写字母命名, 并基于项目名称和目录结构: google_awesome_project. 关于名字空间的讨论和如何命名, 参考 名字空间 一节. 6.8. 枚举命名 枚举的命名应当和 常量 或 宏 一致:kEnumName 或是 ENUM_NAME. 单独的枚举值应该优先采用 常量 的命名方式. 但 宏 方式的命名也可以接受. 枚举名 6.9. 宏命名 ?Tip 你并不打算 使用宏,对吧? 如果你一定要用, 像这样命名:MY_MACRO_THAT_SCARES_SMALL_CHILDREN. 参考 预处理宏;通常 不应该 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线: #defineROUND(x) ... #definePI_ROUNDED 3.0 6.10. 命名规则的特例 如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略. bigopen(): 函数名, 参照open() 的形式 STL 相似实体; 参照STL 命名约定 LONGLONG_MAX: 常量, 如同INT_MAX 译者(acgtyrant)笔记 1.感觉 Google 的命名约定很高明,比如写了简单的类 QueryResult, 接着又可以直接定义一个变量 query_result, 区分度很好;再次,类内变量以下划线结尾,那么就可以直接传入同名的形参,比如 TextQuery::TextQuery(std::stringword) : word_(word) {} , 其中word_ 自然是类内私有成员。 7. 注释 注释虽然写起来很痛苦, 但对保证代码可读性至关重要. 下面的规则描述了如何注释以及在哪儿注释. 当然也要记住: 注释固然很重要, 但最好的代码本身应该是自文档化. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字. 你写的注释是给代码读者看的: 下一个需要理解你的代码的人. 慷慨些吧, 下一个人可能就是你! 7.1. 注释风格 使用 // 或/* */, 统一就好. // 或 /* */ 都可以;但 // 更 常用. 要在如何注释及注释风格上确保统一. 7.2. 文件注释 在每一个文件开头加入版权公告, 然后是文件内容描述. 法律公告和作者信息: 每个文件都应该包含以下项, 依次是: 1.版权声明 (比如,Copyright 2008 Google Inc.) 2.许可证. 为项目选择合适的许可证版本 (比如, Apache 2.0, BSD, LGPL, GPL) 3.作者: 标识文件的原始作者. 如果你对原始作者的文件做了重大修改, 将你的信息添加到作者信息里. 这样当其他人对该文件有疑问时可以知道该联系谁. 文件内容: 紧接着版权许可和作者信息之后, 每个文件都要用注释描述文件内容. 通常, .h 文件要对所声明的类的功能和用法作简单说明. .cc 文件通常包含了更多的实现细节或算法技巧讨论, 如果你感觉这些实现细节或算法技巧讨论对于理解 .h 文件有帮助,可以将该注释挪到 .h, 并在 .cc 中指出文档在 .h. 不要简单的在 .h 和.cc 间复制注释. 这种偏离了注释的实际意义. 每个类的定义都要附带一份注释, 描述类的功能和用法. 7.3类注释 如果你觉得已经在文件顶部详细描述了该类, 想直接简单的来上一句 “完整描述见文件顶部” 也不打紧,但务必确保有这类注释. 如果类有任何同步前提, 文档说明之.如果该类的实例可被多线程访问, 要特别注意文档说明多线程环境下相关的规则和常量使用. 7.4. 函数注释 函数声明处注释描述函数功能; 定义处描述函数实现. 函数声明: 注释位于声明之前, 对函数功能及用法进行描述. 注释使用叙述式 (“Opensthe file”) 而非指令式(“Open the file”); 注释只是为了描述函数, 而不是命令函数做什么. 通常,注释不会描述函数如何工作. 那是函数定义部分的事情. 函数声明处注释的内容: 1.函数的输入输出. 2.对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数. 3.如果函数分配了空间, 需要由调用者释放. 4.参数是否可以为 NULL. 5.是否存在函数使用上的性能隐患. 6.如果函数是可重入的, 其同步前提是什么? 但也要避免罗罗嗦嗦, 或做些显而易见的说明. 下面的注释就没有必要加上 “returnsfalse otherwise”, 因为已经暗含其中了: //Returns true if the table cannot hold any more entries. boolIsTableFull(); 注释构造/析构函数时,切记读代码的人知道构造/析构函数是干啥的, 所以 “destroysthis object” 这样的注释是没有意义的. 注明构造函数对参数做了什么 (例如,是否取得指针所有权) 以及析构函数清理了什么. 如果都是些无关紧要的内容, 直接省掉注释. 析构函数前没有注释是很正常的. 函数定义: 每个函数定义时要用注释说明函数功能和实现要点. 比如说说你用的编程技巧, 实现的大致步骤 或解释如此实现的理由, 为什么前半部分要加锁而后半部分不需要. 不要 从.h 文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上. 7.5. 变量注释 通常变量名本身足以很好说明变量用途. 某些情况下,也需要额外的注释说明. 类数据成员: 每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果变量可以接受 NULL 或 -1 等警戒值,须加以说明. 全局变量: 和数据成员一样, 所有全局变量也要注释说明含义及用途 7.6. 实现注释 对于代码中巧妙的, 晦涩的,有趣的, 重要的地方加以注释. 代码前注释: 巧妙或复杂的代码段前要加注释. 行注释: 比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释如果你需要连续进行多行注释, 可以使之对齐获得更好的可读性: 注意 永远不要 用自然语言翻译代码作为注释. 要假设读代码的人 C++ 水平比你高, 即便他/她可能不知道你的用意: 7.7. 标点, 拼写和语法 注意标点, 拼写和语法;写的好的注释比差的要易读的多. 注释的通常写法是包含正确大小写和结尾句号的完整语句. 短一点的注释 (如代码行尾注释) 可以随意点,依然要注意风格的一致性. 完整的语句可读性更好, 也可以说明该注释是完整的, 而不是一些不成熟的想法. 虽然被别人指出该用分号时却用了逗号多少有些尴尬, 但清晰易读的代码还是很重要的. 正确的标点, 拼写和语法对此会有所帮助. 7.8. TODO 注释 对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释. TODO 注释要使用全大写的字符串 TODO, 在随后的圆括号里写上你的大名, 邮件地址, 或其它身份标识. 冒号是可选的. 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 TODO 格式进行查找. 添加TODO 注释并不意味着你要自己来修正. //TODO([email protected]): Use a "*" here for concatenation operator. //TODO(Zeke) change this to use relations. 如果加 TODO 是为了在 “将来某一天做某事”, 可以附上一个非常明确的时间“Fix by November 2005”), 或者一个明确的事项 (“Remove this code when all clientscan handle XML responses.”). 7.9. 弃用注释 通过弃用注释(DEPRECATED comments)以标记某接口点(interface points)已弃用。 您可以写上包含全大写的 DEPRECATED 的注释,以标记某接口为弃用状态。注释可以放在接口声明前,或者同一行。 在 DEPRECATED 一词后,留下您的名字,邮箱地址以及括号补充。 仅仅标记接口为 DEPRECATED 并不会让大家不约而同地弃用,您还得亲自主动修正调用点(callsites),或是找个帮手。 修正好的代码应该不会再涉及弃用接口点了,着实改用新接口点。如果您不知从何下手,可以找标记弃用注释的当事人一起商量。 8.1. 行长度 每一行代码字符数不超过 80. 我们也认识到这条规则是有争议的, 但很多已有代码都已经遵照这一规则, 我们感觉一致性更重要. 优点: 提倡该原则的人主张强迫他们调整编辑器窗口大小很野蛮. 很多人同时并排开几个代码窗口, 根本没有多余空间拉伸窗口. 大家都把窗口最大尺寸加以限定, 并且 80 列宽是传统标准. 为什么要改变呢? 缺点: 反对该原则的人则认为更宽的代码行更易阅读. 80 列的限制是上个世纪 60 年代的大型机的古板缺陷; 现代设备具有更宽的显示屏, 很轻松的可以显示更多代码. 结论: 80 个字符是最大值. 特例: 1.如果一行注释包含了超过 80 字符的命令或 URL, 出于复制粘贴的方便允许该行超过 80 字符. 2.包含长路径的 #include 语句可以超出80列. 但应该尽量避免. 3.头文件保护 可以无视该原则. 8.2. 非 ASCII 字符 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码. 即使是英文, 也不应将用户界面的文本硬编码到源代码中, 因此非 ASCII 字符要少用. 特殊情况下可以适当包含此类字符. 如, 代码分析外部数据文件时, 可以适当硬编码数据文件中作为分隔符的非 ASCII 字符串; 更常见的是(不需要本地化的) 单元测试代码可能包含非 ASCII 字符串. 此类情况下,应使用 UTF-8 编码, 因为很多工具都可以理解和处理 UTF-8 编码. 十六进制编码也可以, 能增强可读性的情况下尤其鼓励—— 比如"\xEF\xBB\xBF" 在Unicode 中是 零宽度 无间断 的间隔符号, 如果不用十六进制直接放在 UTF-8 格式的源文件中, 是看不到的. (Yang.Y 注: "\xEF\xBB\xBF" 通常用作 UTF-8 with BOM 编码标记) 用 u8 前缀以把带uXXXX 转义序列的字符串字面值编码成 UTF-8. 不要用在本身就带 UTF-8 字符的字符串字面值上,因为如果编译器不把源代码识别成 UTF-8, 输出就会出错。 别用 C++11 的 char16_t 和char32_t, 它们和 UTF-8 文本没有关系,wchar_t 同理,除非您写的代码要调用 Windows API, 后者有用到 wchar_t 扩展。 8.3. 空格还是制表位 只使用空格, 每次缩进2 个空格. 我们使用空格缩进. 不要在代码中使用制符表. 你应该设置编辑器将制符表转为空格. 8.4. 函数声明与定义 返回类型和函数名在同一行, 参数也尽量放在同一行,如果放不下就对形参分行。 函数看上去像这样: ReturnTypeClassName::FunctionName(Type par_name1, Type par_name2) { DoSomething(); ... } 如果同一行文本太多, 放不下所有参数: 注意以下几点: 1.如果返回类型和函数名在一行放不下,分行。 2.如果返回类型那个与函数声明或定义分行了,不要缩进。 3.左圆括号总是和函数名在同一行; 4.函数名和左圆括号间没有空格; 5.圆括号与参数间没有空格; 6.左大括号总在最后一个参数同一行的末尾处; 7.如果其它风格规则允许的话,右大括号总是单独位于函数最后一行,或者与左大括号同一行。 8.右大括号和左大括号间总是有一个空格; 9.函数声明和定义中的所有形参必须有命名且一致; 10.所有形参应尽可能对齐; 11.缺省缩进为 2 个空格; 12.换行后的参数保持 4 个空格的缩进; 如果有些参数没有用到, 在函数定义处将参数名注释起来: 8.5. Lambda 表达式 其它函数怎么格式化形参和函数体,Lambda 表达式就怎么格式化;捕获列表同理。 若用引用捕获,在变量名和 & 之间不留空格。 8.6. 函数调用 要么一行写完函数调用,要么在圆括号里对参数分行,要么参数另起一行且缩进四格。如果没有其它顾虑的话,尽可能精简行数,比如把多个参数适当地放在同一行里。 函数调用遵循如下形式: boolretval = DoSomething(argument1, argument2, argument3); 如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格: 参数也可以放在次行,缩进四格: 把多个参数放在同一行,是为了减少函数调用所需的行数,除非影响到可读性。有人认为把每个参数都独立成行,不仅更好读,而且方便编辑参数。不过,比起所谓的参数编辑,我们更看重可读性,且后者比较好办: 如果一些参数本身就是略复杂的表达式,且降低了可读性。那么可以直接创建临时变量描述该表达式,并传递给函数: 或者放着不管,补充上注释: 如果某参数独立成行,对可读性更有帮助的话,就这么办。 此外,如果一系列参数本身就有一定的结构,可以酌情地按其结构来决定参数格式: 8.7. 列表初始化格式 您平时怎么格式化函数调用,就怎么格式化 5.20. 列表初始化。 如果列表初始化伴随着名字,比如类型或变量名,您可以当名字是函数、{} 是函数调用的括号来格式化它。反之,就当它有个长度为零的名字。 8.8. 条件语句 倾向于不在圆括号内使用空格. 关键字if 和 else 另起一行. 对基本条件语句有两种可以接受的格式. 一种在圆括号和条件之间有空格, 另一种没有. 最常见的是没有空格的格式. 哪种都可以,但 保持一致性.如果你是在修改一个文件, 参考当前已有格式. 如果是写新的代码, 参考目录下或项目中其它文件. 还在徘徊的话, 就不要加空格了. 8.9. 循环和开关选择语句 switch 语句可以使用大括号分段,以表明 cases 之间不是连在一起的。在单语句循环里,括号可用可不用。空循环体应使用 {} 或continue. switch 语句中的 case 块可以使用大括号也可以不用, 取决于你的个人喜好. 如果用的话,要按照下文所述的方法. 如果有不满足 case 条件的枚举值, switch 应该总是包含一个 default 匹配 (如果有输入值没有 case 去处理, 编译器将报警). 如果 default 应该永远执行不到, 简单的加条assert: 8.10. 指针和引用表达式 句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格. 注意: 1.在访问成员时, 句点或箭头前后没有空格. 2.指针操作符 * 或& 后没有空格. 在声明指针变量或参数时, 星号与类型或变量名紧挨都可以: 8.11. 布尔表达式 如果一个布尔表达式超过 标准行宽,断行方式要统一一下. 8.12. 函数返回值 return 表达式里时没必要都用圆括号。 假如您写 x = epr 时本来就会加上括号,那 return expr; 也可如法炮制。 函数返回时不要使用圆括号: 8.13. 变量及数组初始化 用 =, () 和 {} 均可. 您可以用 =, () 和 {}, 以下都对: int x =3; int x(3); int x{3}; stringname("Some Name"); stringname = "Some Name"; stringname{"Some Name"}; 请务必小心列表初始化 {...} 用 std::initializer_list 构造函数初始化出的类型。非空列表初始化就会优先调用 std::initializer_list, 不过空列表初始化除外,后者原则上会调用默认构造函数。为了强制禁用 std::initializer_list 构造函数,请改用括号。 vector vector 此外,列表初始化不允许整型类型的四舍五入,这可以用来避免一些类型上的编程失误。 8.14. 预处理指令 预处理指令不要缩进, 从行首开始. 即使预处理指令位于缩进代码块中, 指令也应从行首开始. 8.15. 类格式 访问控制块的声明依次序是 public:, protected:, private:, 每次缩进 1 个空格. 类声明 注意事项: 1.所有基类名应在 80 列限制下尽量与子类名放在同一行. 2.关键词 public:, protected:, private: 要缩进 1 个空格. 3.除第一个关键词 (一般是public) 外, 其他关键词前要空一行. 如果类比较小的话也可以不空. 4.这些关键词后不要保留空行. 5.public 放在最前面, 然后是protected, 最后是 private. 6.关于声明顺序的规则请参考 声明顺序 一节. 8.16. 构造函数初始值列表 构造函数初始值列表放在同一行或按四格缩进并排几行. 下面两种初始值列表方式都可以接受: // 当全放在一行合适时: MyClass::MyClass(intvar) : some_var_(var), some_other_var_(var + 1) { 或 // 如果要断成多行,缩进四格,冒号放在第一行初始化句: MyClass::MyClass(intvar) : some_var_(var), // 4 空格缩进 some_other_var_(var + 1) { // 对准 ... DoSomething(); ... } 8.17. 名字空间格式化 名字空间内容不缩进. 名字空间 不要增加额外的缩进层次, } // namespace 8.18. 水平留白 水平留白的使用因地制宜. 永远不要在行尾添加没意义的留白. 常规: voidf(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; } // 用括号把大括号与实现分开。 ... 添加冗余的留白会给其他人编辑时造成额外负担. 因此,行尾不要留空格. 如果确定一行代码已经修改完毕, 将多余的空格去掉; 或者在专门清理空格时去掉(确信没有其他人在处理). (Yang.Y 注:现在大部分代码编辑器稍加设置后, 都支持自动删除行首/行尾空格, 如果不支持,考虑换一款编辑器或 IDE) 循环和条件语句: 模板和转换: 8.19. 垂直留白 垂直留白越少越好. 这不仅仅是规则而是原则问题了: 不在万不得已, 不要使用空行. 尤其是:两个函数定义之间的空行不要超过 2 行,函数体首尾不要留空行, 函数体中也不要随意添加空行. 基本原则是: 同一屏可以显示的代码越多, 越容易理解程序的控制流. 当然,过于密集的代码块和过于疏松的代码块同样难看, 取决于你的判断. 但通常是垂直留白越少越好. 空行心得如下: 1.函数体内开头或结尾的空行可读性微乎其微。 2.在多重 if-else 块里加空行或许有点可读性。 9.1. 现有不合规范的代码 对于现有不符合既定编程风格的代码可以网开一面. 当你修改使用其他风格的代码时, 为了与代码原有风格保持一致可以不使用本指南约定. 如果不放心可以与代码原作者或现在的负责人员商讨, 记住,一致性 包括原有的一致性. 9.2. Windows 代码 Windows 程序员有自己的编程习惯, 主要源于 Windows 头文件和其它 Microsoft 代码. 我们希望任何人都可以顺利读懂你的代码, 所以针对所有平台的 C++ 编程只给出一个单独的指南. 如果你习惯使用 Windows 编码风格, 这儿有必要重申一下某些你可能会忘记的指南: 1.不要使用匈牙利命名法 (比如把整型变量命名成 iNum). 使用 Google 命名约定, 包括对源文件使用 .cc 扩展名. 2.Windows定义了很多原生类型的同义词 (YuleFox 注: 这一点,我也很反感), 如DWORD, HANDLE 等等. 在调用Windows API 时这是完全可以接受甚至鼓励的. 但还是尽量使用原有的 C++ 类型, 例如,使用 const TCHAR * 而不是 LPCTSTR. 3.使用 Microsoft Visual C++ 进行编译时, 将警告级别设置为 3 或更高, 并将所有warnings 当作 errors 处理. 4.不要使用 #pragma once; 而应该使用Google 的头文件保护规则. 头文件保护的路径应该相对于项目根目录 (Yang.Y 注: 如#ifndef SRC_DIR_BAR_H_, 参考#define 保护 一节). 5.除非万不得已, 不要使用任何非标准的扩展, 如 #pragma 和 __declspec. 允许使用__declspec(dllimport) 和 __declspec(dllexport); 但你必须通过宏来使用, 比如DLLIMPORT 和 DLLEXPORT, 这样其他人在分享使用这些代码时很容易就去掉这些扩展. 在 Windows 上, 只有很少的一些情况下, 我们可以偶尔违反规则: 6.通常我们 禁止使用多重继承, 但在使用COM 和 ATL/WTL 类时可以使用多重继承. 为了实现COM 或 ATL/WTL 类/接口,你可能不得不使用多重实现继承. 7.虽然代码中不应该使用异常, 但是在ATL 和部分 STL(包括Visual C++ 的 STL) 中异常被广泛使用. 使用ATL 时, 应定义_ATL_NO_EXCEPTIONS 以禁用异常. 你要研究一下是否能够禁用 STL 的异常, 如果无法禁用, 启用编译器异常也可以. (注意这只是为了编译 STL, 自己代码里仍然不要含异常处理.) 8.通常为了利用头文件预编译, 每个每个源文件的开头都会包含一个名为 StdAfx.h 或 precompile.h 的文件. 为了使代码方便与其他项目共享, 避免显式包含此文件 (precompile.cc), 使用 /FI 编译器选项以自动包含. 9.资源头文件通常命名为 resource.h, 且只包含宏的, 不需要遵守本风格指南.