Merge pull request #10 from acgtyrant/master

C++ 风格指南新翻译
This commit is contained in:
Yang.Y
2015-07-31 20:20:32 +08:00
12 changed files with 1663 additions and 930 deletions

View File

@@ -1,5 +1,6 @@
3. 类
------------
TODO
类是 C++ 中代码的基本单元. 显然, 它们被广泛使用. 本节列举了在写一个类时的主要注意事项.
@@ -8,7 +9,7 @@
.. tip::
构造函数中只进行那些没什么意义的 (trivial, YuleFox 注: 简单初始化对于程序执行没有实际的逻辑意义, 因为成员变量 "有意义" 的值大多不在构造函数中确定) 初始化, 可能的话, 使用 ``Init()`` 方法集中初始化有意义的 (non-trivial) 数据.
定义:
在构造函数体中进行初始化操作.
@@ -17,7 +18,7 @@
缺点:
在构造函数中执行操作引起的问题有:
- 构造函数中很难上报错误, `不能使用异常 <#...>`_.
- 操作失败会造成对象初始化失败,进入不确定状态.
- 如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患.
@@ -37,15 +38,15 @@
优点:
默认将结构体初始化为 "无效" 值, 使调试更方便.
缺点:
对代码编写者来说, 这是多余的工作.
结论:
如果类中定义了成员变量, 而且没有提供其它构造函数, 你必须定义一个 (不带参数的) 默认构造函数. 把对象的内部状态初始化成一致/有效的值无疑是更合理的方式.
这么做的原因是: 如果你没有提供其它构造函数, 又没有定义默认构造函数, 编译器将为你自动生成一个. 编译器生成的构造函数并不会对对象进行合理的初始化.
如果你定义的类继承现有类, 而你又没有增加新的成员变量, 则不需要为新类定义默认构造函数.
3.3. 显式构造函数
@@ -59,13 +60,13 @@
优点:
避免不合时宜的变换.
缺点:
结论:
所有单参数构造函数都必须是显式的. 在类定义中, 将关键字 ``explicit`` 加到单参数构造函数前: ``explicit Foo(string name);``
例外: 在极少数情况下, 拷贝构造函数可以不声明成 ``explicit``. 作为其它类的透明包装器的类也是特例之一. 类似的例外情况应在注释中明确说明.
.. _copy-constructors:
@@ -75,29 +76,29 @@
.. tip::
仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数; 大部分情况下都不需要, 此时应使用 ``DISALLOW_COPY_AND_ASSIGN``.
定义:
拷贝构造函数在复制一个对象到新建对象时被调用 (特别是对象传值时).
优点:
拷贝构造函数使得拷贝对象更加容易. STL 容器要求所有内容可拷贝, 可赋值.
缺点:
C++ 中的隐式对象拷贝是很多性能问题和 bug 的根源. 拷贝构造函数降低了代码可读性, 相比传引用, 跟踪传值的对象更加困难, 对象修改的地方变得难以捉摸.
结论:
大部分类并不需要可拷贝, 也不需要一个拷贝构造函数或重载赋值运算符. 不幸的是, 如果你不主动声明它们, 编译器会为你自动生成, 而且是 ``public`` 的.
可以考虑在类的 ``private:`` 中添加拷贝构造函数和赋值操作的空实现, 只有声明, 没有定义. 由于这些空函数声明为 ``private``, 当其他代码试图使用它们的时候, 编译器将报错. 方便起见, 我们可以使用 ``DISALLOW_COPY_AND_ASSIGN`` 宏:
.. code-block:: c++
// 禁止使用拷贝构造函数和 operator= 赋值操作的宏
// 应该类的 private: 中使用
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
``class foo:`` 中:
.. code-block:: c++
@@ -105,11 +106,11 @@
public:
Foo(int f);
~Foo();
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};
如上所述, 绝大多数情况下都应使用 ``DISALLOW_COPY_AND_ASSIGN`` 宏. 如果类确实需要可拷贝, 应在该类的头文件中说明原由, 并合理的定义拷贝构造函数和赋值操作. 注意在 ``operator=`` 中检测自我赋值的情况 (yospaly 注: 即 ``operator=`` 接收的参数是该对象本身).
为了能作为 STL 容器的值, 你可能有使类可拷贝的冲动. 在大多数类似的情况下, 真正该做的是把对象的 *指针* 放到 STL 容器中. 可以考虑使用 ``std::tr1::shared_ptr``.
@@ -121,8 +122,8 @@
.. tip::
仅当只有数据时使用 ``struct``, 其它一概使用 ``class``.
在 C++ 中 ``struct````class`` 关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便定义的数据类型选择合适的关键字.
在 C++ 中 ``struct````class`` 关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便定义的数据类型选择合适的关键字.
``struct`` 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域 (field), 而非函数调用. 除了构造函数, 析构函数, ``Initialize()``, ``Reset()``, ``Validate()`` 外, 不能提供其它功能的函数.
@@ -151,11 +152,11 @@
结论:
所有继承必须是 ``public`` 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式.
不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 "是一个" ("is-a", YuleFox 注: 其他 "has-a" 情况下请使用组合) 的情况下使用继承: 如果 ``Bar`` 的确 "是一种" Foo, ``Bar`` 才能继承 ``Foo``.
必要的话, 析构函数声明为 ``virtual``. 如果你的类有虚函数, 则析构函数也应该为虚函数. 注意 `数据成员在任何情况下都必须是私有的 <....>`_.
当重载一个虚函数, 在衍生类中把它明确的声明为 ``virtual``. 理论依据: 如果省略 ``virtual`` 关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数.
.. _multiple-inheritance:
@@ -171,13 +172,13 @@
优点:
相比单继承 (见 :ref:`继承 <inheritance>`), 多重实现继承可以复用更多的代码.
缺点:
真正需要用到多重 *实现* 继承的情况少之又少. 多重实现继承看上去是不错的解决方案, 但你通常也可以找到一个更明确, 更清晰的不同解决方案.
结论:
只有当所有父类除第一个外都是 :ref:`纯接口类 <interface>` 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以 ``Interface`` 为后缀.
.. note::
关于该规则, Windows 下有个 :ref:`特例 <windows-code>`.
@@ -189,23 +190,23 @@
.. tip::
接口是指满足特定条件的类, 这些类以 ``Interface`` 为后缀 (不强制).
定义:
当一个类满足以下要求时, 称之为纯接口:
- 只有纯虚函数 ("``=0``") 和静态函数 (除了下文提到的析构函数).
- 没有非静态数据成员.
- 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 ``protected``.
- 如果它是一个子类, 也只能从满足上述条件并以 ``Interface`` 为后缀的类继承.
接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数 (作为上述第 1 条规则的特例, 析构函数不能是纯虚函数). 具体细节可参考 Stroustrup 的 *The C++ Programming Language, 3rd edition* 第 12.4 节.
优点:
``Interface`` 为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员. 这一点对于 :ref:`多重继承 <multiple-inheritance>` 尤其重要. 另外, 对于 Java 程序员来说, 接口的概念已是深入人心.
缺点:
``Interface`` 后缀增加了类名长度, 为阅读和理解带来不便. 同时,接口特性作为实现细节不应暴露给用户.
结论:
只有在满足上述需要时, 类才以 ``Interface`` 结尾, 但反过来, 满足上述需要的类未必一定以 ``Interface`` 结尾.
@@ -214,16 +215,16 @@
.. tip::
除少数特定环境外,不要重载运算符.
定义:
一个类可以定义诸如 ``+````/`` 等运算符, 使其可以像内建类型一样直接操作.
优点:
使代码看上去更加直观, 类表现的和内建类型 (如 ``int``) 行为一致. 重载运算符使 ``Equals()``, ``Add()`` 等函数名黯然失色. 为了使一些模板函数正确工作, 你可能必须定义操作符.
缺点:
虽然操作符重载令代码更加直观, 但也有一些不足:
- 混淆视听, 让你误以为一些耗时的操作和操作内建类型一样轻巧.
- 更难定位重载运算符的调用点, 查找 ``Equals()`` 显然比对应的 ``==`` 调用点要容易的多.
- 有的运算符可以对指针进行操作, 容易导致 bug. ``Foo + 4`` 做的是一件事, 而 ``&Foo + 4`` 可能做的是完全不同的另一件事. 对于二者, 编译器都不会报错, 使其很难调试;
@@ -232,13 +233,13 @@
结论:
一般不要重载运算符. 尤其是赋值操作 (``operator=``) 比较诡异, 应避免重载. 如果需要的话, 可以定义类似 ``Equals()``, ``CopyFrom()`` 等函数.
然而, 极少数情况下可能需要重载运算符以便与模板或 "标准" C++ 类互操作 (如 ``operator<<(ostream&, const T&)``). 只有被证明是完全合理的才能重载, 但你还是要尽可能避免这样做. 尤其是不要仅仅为了在 STL 容器中用作键值就重载 ``operator==````operator<``; 相反, 你应该在声明容器的时候, 创建相等判断和大小比较的仿函数类型.
有些 STL 算法确实需要重载 ``operator==`` 时, 你可以这么做, 记得别忘了在文档中说明原因.
参考 :ref:`拷贝构造函数 <copy-constructors>`:ref:`函数重载 <function-overloading>`.
3.10. 存取控制
~~~~~~~~~~~~~~~~~~~~~
@@ -256,11 +257,11 @@
.. tip::
在类中使用特定的声明顺序: ``public:````private:`` 之前, 成员函数在数据成员 (变量) 前;
类的访问控制区段的声明顺序依次为: ``public:``, ``protected:``, ``private:``. 如果某区段没内容, 可以不声明.
每个区段内的声明通常按以下顺序:
- ``typedefs`` 和枚举
- 常量
- 构造函数
@@ -279,7 +280,7 @@
.. tip::
倾向编写简短, 凝练的函数.
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题. 甚至导致难以发现的 bug. 使函数尽量简短, 便于他人阅读和修改代码.
@@ -300,4 +301,4 @@
#. 为降低复杂性, 尽量不重载操作符, 模板, 标准类中使用时提供文档说明;
#. 存取函数一般内联在头文件中;
#. 声明次序: ``public`` -> ``protected`` -> ``private``;
#. 函数体尽量短小, 紧凑, 功能单一;
#. 函数体尽量短小, 紧凑, 功能单一;

View File

@@ -1,52 +1,57 @@
7. 注释
------------
------
注释虽然写起来很痛苦, 但对保证代码可读性至关重要. 下面的规则描述了如何注释以及在哪儿注释. 当然也要记住: 注释固然很重要, 但最好的代码本身应该是自文档化. 有意义的类型名和变量名, 要远胜过要用注释解释的含糊不清的名字.
你写的注释是给代码读者看的: 下一个需要理解你的代码的人. 慷慨些吧, 下一个人可能就是你!
7.1. 注释风格
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
使用 ``//````/* */``, 统一就好.
``//````/* */`` 都可以; 但 ``//`` *更* 常用. 要在如何注释及注释风格上确保统一.
7.2. 文件注释
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
在每一个文件开头加入版权公告, 然后是文件内容描述.
法律公告和作者信息:
每个文件都应该包含以下项, 依次是:
- 版权声明 (比如, ``Copyright 2008 Google Inc.``)
- 许可证. 为项目选择合适的许可证版本 (比如, Apache 2.0, BSD, LGPL, GPL)
- 作者: 标识文件的原始作者.
如果你对原始作者的文件做了重大修改, 将你的信息添加到作者信息里. 这样当其他人对该文件有疑问时可以知道该联系谁.
文件内容:
紧接着版权许可和作者信息之后, 每个文件都要用注释描述文件内容.
通常, ``.h`` 文件要对所声明的类的功能和用法作简单说明. ``.cc`` 文件通常包含了更多的实现细节或算法技巧讨论, 如果你感觉这些实现细节或算法技巧讨论对于理解 ``.h`` 文件有帮助, 可以该注释挪到 ``.h``, 并在 ``.cc`` 中指出文档在 ``.h``.
不要简单的在 ``.h````.cc`` 间复制注释. 这种偏离了注释的实际意义.
.. _class-comments:
7.3. 类注释
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~
.. tip::
每个类的定义都要附带一份注释, 描述类的功能和用法.
.. code-block:: c++
// Iterates over the contents of a GargantuanTable. Sample usage:
// GargantuanTable_Iterator* iter = table->NewIterator();
// for (iter->Seek("foo"); !iter->done(); iter->Next()) {
@@ -56,33 +61,35 @@
class GargantuanTable_Iterator {
...
};
如果你觉得已经在文件顶部详细描述了该类, 想直接简单的来上一句 "完整描述见文件顶部" 也不打紧, 但务必确保有这类注释.
如果类有任何同步前提, 文档说明之. 如果该类的实例可被多线程访问, 要特别注意文档说明多线程环境下相关的规则和常量使用.
7.4. 函数注释
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
函数声明处注释描述函数功能; 定义处描述函数实现.
函数声明:
注释位于声明之前, 对函数功能及用法进行描述. 注释使用叙述式 ("Opens the file") 而非指令式 ("Open the file"); 注释只是为了描述函数, 而不是命令函数做什么. 通常, 注释不会描述函数如何工作. 那是函数定义部分的事情.
函数声明处注释的内容:
- 函数的输入输出.
- 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
- 如果函数分配了空间, 需要由调用者释放.
- 参数是否可以为 ``NULL``.
- 是否存在函数使用上的性能隐患.
- 如果函数是可重入的, 其同步前提是什么?
举例如下:
.. code-block:: c++
// Returns an iterator for this table. It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
@@ -98,32 +105,35 @@
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;
但也要避免罗罗嗦嗦, 或做些显而易见的说明. 下面的注释就没有必要加上 "returns false otherwise", 因为已经暗含其中了:
.. code-block:: c++
// Returns true if the table cannot hold any more entries.
bool IsTableFull();
注释构造/析构函数时, 切记读代码的人知道构造/析构函数是干啥的, 所以 "destroys this object" 这样的注释是没有意义的. 注明构造函数对参数做了什么 (例如, 是否取得指针所有权) 以及析构函数清理了什么. 如果都是些无关紧要的内容, 直接省掉注释. 析构函数前没有注释是很正常的.
函数定义:
每个函数定义时要用注释说明函数功能和实现要点. 比如说说你用的编程技巧, 实现的大致步骤, 或解释如此实现的理由, 为什么前半部分要加锁而后半部分不需要.
*不要*``.h`` 文件或其他地方的函数声明处直接复制注释. 简要重述函数功能是可以的, 但注释重点要放在如何实现上.
7.5. 变量注释
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
通常变量名本身足以很好说明变量用途. 某些情况下, 也需要额外的注释说明.
类数据成员:
每个类数据成员 (也叫实例变量或成员变量) 都应该用注释说明用途. 如果变量可以接受 ``NULL````-1`` 等警戒值, 须加以说明. 比如:
.. code-block:: c++
private:
// Keeps track of the total number of entries in the table.
// Used to ensure we do not go over the limit. -1 means
@@ -132,25 +142,27 @@
全局变量:
和数据成员一样, 所有全局变量也要注释说明含义及用途. 比如:
.. code-block:: c++
// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;
7.6. 实现注释
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释.
代码前注释:
巧妙或复杂的代码段前要加注释. 比如:
.. code-block:: c++
// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
@@ -160,21 +172,22 @@
}
行注释:
比较隐晦的地方要在行尾加入注释. 在行尾空两格进行注释. 比如:
.. code-block:: c++
// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
return; // Error already logged.
注意, 这里用了两段注释分别描述这段代码的作用, 和提示函数返回时错误已经被记入日志.
如果你需要连续进行多行注释, 可以使之对齐获得更好的可读性:
.. code-block:: c++
DoSomething(); // Comment here so the comments line up.
DoSomethingElseThatIsLonger(); // Comment here so there are two spaces between
// the code and the comment.
@@ -184,31 +197,31 @@
}
NULL, true/false, 1, 2, 3...:
向函数传入 ``NULL``, 布尔值或整数时, 要注释说明含义, 或使用常量让代码望文知意. 例如, 对比:
.. warning::
.. code-block:: c++
bool success = CalculateSomething(interesting_value,
10,
false,
NULL); // What are these arguments??
和:
.. code-block:: c++
bool success = CalculateSomething(interesting_value,
10, // Default base value.
false, // Not the first time we're calling this.
NULL); // No callback.
或使用常量或描述性变量:
.. code-block:: c++
const int kDefaultBaseValue = 10;
const bool kFirstTimeCalling = false;
Callback *null_callback = NULL;
@@ -218,46 +231,65 @@ NULL, true/false, 1, 2, 3...:
null_callback);
不允许:
注意 *永远不要* 用自然语言翻译代码作为注释. 要假设读代码的人 C++ 水平比你高, 即便他/她可能不知道你的用意:
.. warning::
.. code-block:: c++
// 现在, 检查 b 数组并确保 i 是否存在,
// 下一个元素是 i+1.
... // 天哪. 令人崩溃的注释.
7.7. 标点, 拼写和语法
~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~
.. tip::
注意标点, 拼写和语法; 写的好的注释比差的要易读的多.
注释的通常写法是包含正确大小写和结尾句号的完整语句. 短一点的注释 (如代码行尾注释) 可以随意点, 依然要注意风格的一致性. 完整的语句可读性更好, 也可以说明该注释是完整的, 而不是一些不成熟的想法.
虽然被别人指出该用分号时却用了逗号多少有些尴尬, 但清晰易读的代码还是很重要的. 正确的标点, 拼写和语法对此会有所帮助.
7.8. TODO 注释
~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~
.. tip::
对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 ``TODO`` 注释.
``TODO`` 注释要使用全大写的字符串 ``TODO``, 在随后的圆括号里写上你的大名, 邮件地址, 或其它身份标识. 冒号是可选的. 主要目的是让添加注释的人 (也是可以请求提供更多细节的人) 可根据规范的 ``TODO`` 格式进行查找. 添加 ``TODO`` 注释并不意味着你要自己来修正.
.. code-block:: c++
// TODO(kl@gmail.com): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.
如果加 ``TODO`` 是为了在 "将来某一天做某事", 可以附上一个非常明确的时间 "Fix by November 2005"), 或者一个明确的事项 ("Remove this code when all clients can handle XML responses.").
译者 (YuleFox) 笔记
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7.9. 弃用注释
~~~~~~~~~~~
1. 关于注释风格,很多 C++ 的 coders 更喜欢行注释, C coders 或许对块注释依然情有独钟, 或者在文件头大段大段的注释时使用块注释;
2. 文件注释可以炫耀你的成就, 也是为了捅了篓子别人可以找你;
3. 注释要言简意赅, 不要拖沓冗余, 复杂的东西简单化和简单的东西复杂化都是要被鄙视的;
4. 对于 Chinese coders 来说, 用英文注释还是用中文注释, it is a problem, 但不管怎样, 注释是为了让别人看懂, 难道是为了炫耀编程语言之外的你的母语或外语水平吗;
5. 注释不要太乱, 适当的缩进才会让人乐意看. 但也没有必要规定注释从第几列开始 (我自己写代码的时候总喜欢这样), UNIX/LINUX 下还可以约定是使用 tab 还是 space, 个人倾向于 space;
6. TODO 很不错, 有时候, 注释确实是为了标记一些未完成的或完成的不尽如人意的地方, 这样一搜索, 就知道还有哪些活要干, 日志都省了.
.. tip::
通过弃用注释(``DEPRECATED`` comments以标记某接口点interface points已弃用。
您可以写上包含全大写的 ``DEPRECATED`` 注释,以标记某接口为弃用状态。注释可以放在接口声明前,或者同一行。
``DEPRECATED`` 一词后,留下您的名字,邮箱地址以及括号补充。
仅仅标记接口为 ``DEPRECATED`` 并不会让大家不约而同地弃用您还得亲自主动修正调用点callsites或是找个帮手。
修正好的代码应该不会再涉及弃用接口点了,着实改用新接口点。如果您不知从何下手,可以找标记弃用注释的当事人一起商量。
译者 (YuleFox) 笔记
~~~~~~~~~~~~~~~~~
#. 关于注释风格,很多 C++ 的 coders 更喜欢行注释, C coders 或许对块注释依然情有独钟, 或者在文件头大段大段的注释时使用块注释;
#. 文件注释可以炫耀你的成就, 也是为了捅了篓子别人可以找你;
#. 注释要言简意赅, 不要拖沓冗余, 复杂的东西简单化和简单的东西复杂化都是要被鄙视的;
#. 对于 Chinese coders 来说, 用英文注释还是用中文注释, it is a problem, 但不管怎样, 注释是为了让别人看懂, 难道是为了炫耀编程语言之外的你的母语或外语水平吗;
#. 注释不要太乱, 适当的缩进才会让人乐意看. 但也没有必要规定注释从第几列开始 (我自己写代码的时候总喜欢这样), UNIX/LINUX 下还可以约定是使用 tab 还是 space, 个人倾向于 space;
#. TODO 很不错, 有时候, 注释确实是为了标记一些未完成的或完成的不尽如人意的地方, 这样一搜索, 就知道还有哪些活要干, 日志都省了.

View File

@@ -2,9 +2,8 @@
.. _cpp_contents:
C++ 风格指南 - 内容目录
==========================
====================
.. contents::
:backlinks: none

View File

@@ -1,5 +1,5 @@
10. 结束语
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~
.. tip::
@@ -10,13 +10,3 @@
风格指南的重点在于提供一个通用的编程规范, 这样大家可以把精力集中在实现内容而不是表现形式上. 我们展示了全局的风格规范, 但局部风格也很重要, 如果你在一个文件中新加的代码和原有代码风格相去甚远, 这就破坏了文件本身的整体美观, 也影响阅读, 所以要尽量避免.
好了, 关于编码风格写的够多了; 代码本身才更有趣. 尽情享受吧!
::
Revision 3.133
Benjy Weinberger
Craig Silverstein
Gregory Eitzmann
Mark Mentovai
Tashana Landray

View File

@@ -1,43 +1,45 @@
9. 规则特例
----------------
---------
前面说明的编程习惯基本都是强制性的. 但所有优秀的规则都允许例外, 这里就是探讨这些特例.
9.1. 现有不合规范的代码
~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~
.. tip::
对于现有不符合既定编程风格的代码可以网开一面.
当你修改使用其他风格的代码时, 为了与代码原有风格保持一致可以不使用本指南约定. 如果不放心可以与代码原作者或现在的负责人员商讨, 记住, *一致性* 包括原有的一致性.
.. _windows-code:
9.2. Windows 代码
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~
.. tip::
Windows 程序员有自己的编程习惯, 主要源于 Windows 头文件和其它 Microsoft 代码. 我们希望任何人都可以顺利读懂你的代码, 所以针对所有平台的 C++ 编程只给出一个单独的指南.
如果你习惯使用 Windows 编码风格, 这儿有必要重申一下某些你可能会忘记的指南:
- 不要使用匈牙利命名法 (比如把整型变量命名成 ``iNum``). 使用 Google 命名约定, 包括对源文件使用 ``.cc`` 扩展名.
- Windows 定义了很多原生类型的同义词 (YuleFox 注: 这一点, 我也很反感), 如 ``DWORD``, ``HANDLE`` 等等. 在调用 Windows API 时这是完全可以接受甚至鼓励的. 但还是尽量使用原有的 C++ 类型, 例如, 使用 ``const TCHAR *`` 而不是 ``LPCTSTR``.
- 使用 Microsoft Visual C++ 进行编译时, 将警告级别设置为 3 或更高, 并将所有 warnings 当作 errors 处理.
- 不要使用 ``#pragma once``; 而应该使用 Google 的头文件保护规则. 头文件保护的路径应该相对于项目根目录 (yospaly 注: 如 ``#ifndef SRC_DIR_BAR_H_``, 参考 :ref:`#define 保护 <define_guard>` 一节).
- 除非万不得已, 不要使用任何非标准的扩展, 如 ``#pragma````__declspec``. 允许使用 ``__declspec(dllimport)````__declspec(dllexport)``; 但你必须通过宏来使用, 比如 ``DLLIMPORT````DLLEXPORT``, 这样其他人在分享使用这些代码时很容易就去掉这些扩展.
在 Windows 上, 只有很少的一些情况下, 我们可以偶尔违反规则:
- 通常我们 :ref:`禁止使用多重继承 <multiple-inheritance>`, 但在使用 COM 和 ATL/WTL 类时可以使用多重继承. 为了实现 COM 或 ATL/WTL 类/接口, 你可能不得不使用多重实现继承.
- 虽然代码中不应该使用异常, 但是在 ATL 和部分 STL包括 Visual C++ 的 STL) 中异常被广泛使用. 使用 ATL 时, 应定义 ``_ATL_NO_EXCEPTIONS`` 以禁用异常. 你要研究一下是否能够禁用 STL 的异常, 如果无法禁用, 启用编译器异常也可以. (注意这只是为了编译 STL, 自己代码里仍然不要含异常处理.)
- 通常为了利用头文件预编译, 每个每个源文件的开头都会包含一个名为 ``StdAfx.h````precompile.h`` 的文件. 为了使代码方便与其他项目共享, 避免显式包含此文件 (``precompile.cc``), 使用 ``/FI`` 编译器选项以自动包含.
- 资源头文件通常命名为 ``resource.h``, 且只包含宏的, 不需要遵守本风格指南.

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
1. 头文件
------------
--------
通常每一个 ``.cc`` 文件都有一个对应的 ``.h`` 文件. 也有一些常见例外, 如单元测试代码和只包含 ``main()`` 函数的 ``.cc`` 文件.
@@ -7,13 +7,31 @@
下面的规则将引导你规避使用头文件时的各种陷阱.
.. _define_guard:
.. _self-contained headers:
1.1. #define 保护
~~~~~~~~~~~~~~~~~~~~
1.1. Self-contained 头文件
~~~~~~~~~~~~~~~~~~~~~~~~~
.. tip::
所有头文件都应该使用 ``#define`` 防止头文件被多重包含, 命名格式当是: ``<PROJECT>_<PATH>_<FILE>_H_``
头文件应该能够自给自足self-contained``in.h`` 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 ``.inc`` 结尾。
所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有 :ref:`define-guard`,统统包含它所需要的其它头文件,也不要求定义任何特别 symbols.
不过有一个例外,即一个文件并不是 self-contained 的而是用来安插到代码某处里特别是要安插多次的时候。或者文件内容实际上是其它头文件的特定平台platform-specific扩展部分。这些文件就要用 ``.inc`` 文件扩展名。
如果 ``.h`` 文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 ``.cc`` 文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。现在不要把这些定义放到分离的 -inl.h 文件里了(译者注:过去该规范曾提倡把定义放到 -inl.h 里过)。
As an exception, a function template that is explicitly instantiated for all relevant sets of template arguments, or that is a private member of a class, may be defined in the only .cc file that instantiates the template. TODO
.. _define-guard:
1.2. #define 保护
~~~~~~~~~~~~~~~~
.. tip::
所有头文件都应该使用 ``#define`` 防止头文件被多重包含, 命名格式当是: ``<PROJECT>_<PATH>_<FILE>_H_``
为保证唯一性, 头文件的命名应该依据所在项目源代码树的全路径. 例如, 项目 ``foo`` 中的头文件 ``foo/src/bar/baz.h`` 可按如下方式保护:
@@ -24,93 +42,99 @@
#endif // FOO_BAR_BAZ_H_
.. _forward-declarations:
1.2. 头文件依赖
~~~~~~~~~~~~~~~~~~~~
1.3. 前向声明
~~~~~~~~~~~
.. tip::
能用前置声明的地方尽量不使用 ``#include``.
当一个头文件被包含的同时也引入了新的依赖, 一旦该头文件被修改, 代码就会被重新编译. 如果这个头文件又包含了其他头文件, 这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译. 因此, 我们倾向于减少包含头文件, 尤其是在头文件中包含头文件.
您可以靠前置声明来避免多余的 ``#includes``.
使用前置声明可以显著减少需要包含的头文件数量. 举例说明: 如果头文件中用到类 ``File``, 但不需要访问 ``File`` 类的声明, 头文件中只需前置声明 ``class File;`` 而无须 ``#include "file/base/file.h"``.
定义:
不允许访问类的定义的前提下, 我们在一个头文件中能对类 ``Foo`` 做哪些操作?
所谓「前向声明」forward declaration是类函数和模板的纯粹声明没伴随着其定义。代码中用到了哪些 symbols, 往往可以用其前向声明来代替对应的 ``#inclues``.
- 我们可以将数据成员类型声明为 ``Foo *````Foo &``.
- 我们可以将函数参数 / 返回值的类型声明为 ``Foo`` (但不能定义实现).
- 我们可以将静态数据成员的类型声明为 ``Foo``, 因为静态数据成员的定义在类定义之外.
优点:
反之, 如果你的类是 ``Foo`` 的子类, 或者含有类型为 ``Foo`` 的非静态数据成员, 则必须包含 ``Foo`` 所在的头文件.
* 多余的 ``#includes`` 会害得编译器花费不少时间展开更多文件,处理大量输入。
* 而且一旦改动头文件,就得重新编译整个文件。
有时, 使用指针成员 (如果是 ``scoped_ptr`` 更好) 替代对象成员的确是明智之选. 然而, 这会降低代码可读性及执行效率, 因此如果仅仅为了少包含头文件,还是不要这么做的好.
缺点:
当然 ``.cc`` 文件无论如何都需要所使用类的定义部分, 自然也就会包含若干头文件.
* 如果前向声明关系到模板typedefs, 默认参数和 using 声明,就很难决定它的具体样子了。
* 很难判断什么时候该用前向声明,什么时候该用 ``#includes``, 特别是涉及隐式转换运算符的时候。极端情况下,用前向声明代替 ``includes`` 甚至都会暗暗地改变代码的含义。
* 前向声明了不少来自头文件的 symbol 时,就会比单单 ``includes`` 一行冗长。
* 前向声明函数或模板有时会害得头文件开发者难以轻易变动其 API. 就像扩大形参类型,加个自带默认参数的模板形参等等。
* 前向声明来自命名空间 ``std::` 的 symbol 时,其行为未定义。
* 仅仅为了能前向声明而重构代码(比如用指针成员代替对象成员),后者会变慢且复杂起来。
* 还没有实践证实前向声明的优越性。
结论:
* 函数:用 ``#include``.
* 类模板:优先用 ``#includes``.
* 类:用前向声明固然不错,但小心点。若说不定,还是用 ``#includes`` 好了。
* 千万别为了避免 ``includes`` 而把数据成员改成指针。
至于什么时候包含头文件,参见 :ref:`name-and-order-of-includes`
.. _inline-functions:
1.3. 内联函数
~~~~~~~~~~~~~~~~~~~~
1.4. 内联函数
~~~~~~~~~~~
.. tip::
只有当函数只有 10 行甚至更少时才将其定义为内联函数.
定义:
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
优点:
当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点:
滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论:
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 ``switch`` 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 ``switch`` 语句从不被执行).
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
.. _inl-files:
1.4. -inl.h文件
~~~~~~~~~~~~~~~~~~~~
.. tip::
复杂的内联函数的定义, 应放在后缀名为 ``-inl.h`` 的头文件中.
内联函数的定义必须放在头文件中, 编译器才能在调用点内联展开定义. 然而, 实现代码理论上应该放在 ``.cc`` 文件中, 我们不希望 ``.h`` 文件中有太多实现代码, 除非在可读性和性能上有明显优势.
如果内联函数的定义比较短小, 逻辑比较简单, 实现代码放在 ``.h`` 文件里没有任何问题. 比如, 存取函数的实现理所当然都应该放在类定义内. 出于编写者和调用者的方便, 较复杂的内联函数也可以放到 ``.h`` 文件中, 如果你觉得这样会使头文件显得笨重, 也可以把它萃取到单独的 ``-inl.h`` 中. 这样把实现和类定义分离开来, 当需要时包含对应的 ``-inl.h`` 即可。
``-inl.h`` 文件还可用于函数模板的定义. 从而增强模板定义的可读性.
别忘了 ``-inl.h`` 和其他头文件一样, 也需要 ``#define`` 保护.
1.5. 函数参数的顺序
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~
.. tip::
定义函数时, 参数顺序依次为: 输入参数, 然后是输出参数.
C/C++ 函数参数分为输入参数, 输出参数, 和输入/输出参数三种. 输入参数一般传值或传 ``const`` 引用, 输出参数或输入/输出参数则是非-``const`` 指针. 对参数排序时, 将只输入的参数放在所有输出参数之前. 尤其是不要仅仅因为是新加的参数, 就把它放在最后; 即使是新加的只输入参数也要放在输出参数之前.
这条规则并不需要严格遵守. 输入/输出两用参数 (通常是类/结构体变量) 把事情变得复杂, 为保持和相关函数的一致性, 你有时不得不有所变通.
.. _name-and-order-of-includes
1.6. ``#include`` 的路径及顺序
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. tip::
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: C 库, C++ 库, 其他库的 `.h`, 本项目内的 `.h`.
使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 `.h`, 本项目内的 `.h`.
项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 ``.`` (当前目录) 或 ``..`` (上级目录). 例如, ``google-awesome-project/src/base/logging.h`` 应该按如下方式包含:
.. code-block:: c++
#include “base/logging.h”
又如, ``dir/foo.cc`` 的主要作用是实现或测试 ``dir2/foo2.h`` 的功能, ``foo.cc`` 中包含头文件的次序如下:
#. ``dir2/foo2.h`` (优先位置, 详情如下)
#. C 系统文件
#. C++ 系统文件
@@ -123,20 +147,39 @@ C/C++ 函数参数分为输入参数, 输出参数, 和输入/输出参数三种
按字母顺序对头文件包含进行二次排序是不错的主意 (yospaly 译注: 之前已经按头文件类别排过序了).
您所依赖的 symbols 被哪些头文件所定义您就应该包含include哪些头文件:ref:`forward-declaration` 情况除外。比如您要用到 ``bar.h`` 中的某个 symbol, 哪怕您所包含的 ``foo.h`` 已经包含了 ``bar.h``, 也照样得包含 ``bar.h``, 除非 ``foo.h`` 有明确说明它会自动向您提供 ``bar.h`` 中的 symbol. 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 ``foo.cc`` 只包含 ``foo.h`` 就够了,不用再管后者所包含的其它内容。
举例来说, ``google-awesome-project/src/foo/internal/fooserver.cc`` 的包含次序如下:
.. code-block:: c++
#include "foo/public/fooserver.h" // 优先位置
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
.. code-block:: c++
#include "foo/public/fooserver.h" // 优先位置
#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>
#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"
例外:
有时平台特定system-specific代码需要条件编译conditional includes这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:
.. code-block:: c++
#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
译者 (YuleFox) 笔记
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~
#. 避免多重包含是学编程时最基本的要求;
#. 前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;
@@ -144,3 +187,12 @@ C/C++ 函数参数分为输入参数, 输出参数, 和输入/输出参数三种
#. ``-inl.h`` 可提高代码可读性 (一般用不到吧:D);
#. 标准化函数参数顺序可以提高可读性和易维护性 (对函数参数的堆栈空间有轻微影响, 我以前大多是相同类型放在一起);
#. 包含文件的名称使用 ``.````..`` 虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理, 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在 "最需要编译" (对应源文件处 :D) 的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了.
译者acgtyrant笔记
~~~~~~~~~~~~~~~~~~~
#. 原来还真有项目用 ``#includes`` 来插入文本,且其文件扩展名 ``.inc`` 看上去也很科学。
#. Google 已经不再提倡 ``-inl.h`` 用法。
#. 注意前向声明的类是不完全类型incomplete type我们只能定义指向该类型的指针或引用或者声明但不能定义以不完全类型作为参数或者返回类型的函数。毕竟编译器不知道不完全类型的定义我们不能创建其类的任何对象也不能声明成类内部的数据成员。
#. 类内部的函数一般会自动内联。所以某函数一旦不需要内联,其定义就不要再放在头文件里,而是放到对应的 ``.cc`` 文件里。这样可以保持头文件的类相当精炼,也很好地贯彻了声明与定义分离的原则。
#.``#include`` 中插入空行以分割相关头文件, C 库, C++ 库, 其他库的 `.h` 和本项目内的 `.h`.是个好习惯。

View File

@@ -1,9 +1,10 @@
0. 扉页
================================
======
:版本: 3.133
:版本: 4.45
:原作者:
.. line-block::
Benjy Weinberger
@@ -13,21 +14,25 @@
Tashana Landray
:翻译:
.. line-block::
`YuleFox <http://www.yulefox.com>`_
`brantyoung <http://yangyubo.com>`_
`acgtyrant <http://acgtyrant.com>`_
:项目主页:
- `Google Style Guide <http://google-styleguide.googlecode.com>`_
- `Google 开源项目风格指南 - 中文版 <http://github.com/zh-google-styleguide/zh-google-styleguide>`_
0.1 译者前言
---------------
----------
Google 经常会发布一些开源项目, 意味着会接受来自其他代码贡献者的代码. 但是如果代码贡献者的编程风格与 Google 的不一致, 会给代码阅读者和其他代码提交这造成不小的困扰. Google 因此发布了这份自己的编程风格, 使所有提交代码的人都能获知 Google 的编程风格.
翻译初衷:
规则的作用就是避免混乱. 但规则本身一定要权威, 有说服力, 并且是理性的. 我们所见过的大部分编程规范, 其内容或不够严谨, 或阐述过于简单, 或带有一定的武断性.
Google 保持其一贯的严谨精神, 5 万汉字的指南涉及广泛, 论证严密. 我们翻译该系列指南的主因也正是其严谨性. 严谨意味着指南的价值不仅仅局限于它罗列出的规范, 更具参考意义的是它为了列出规范而做的谨慎权衡过程.
@@ -41,6 +46,9 @@ Google 经常会发布一些开源项目, 意味着会接受来自其他代码
中文版和英文版一样, 使用 ``Artistic License/GPL`` 开源许可.
中文版修订历史:
- 2015-07 4.45 : acgtyrant 为了学习 C++ 的规范,顺便重新翻译了本 C++ 风格指南,特别是 C++11 的全新内容。排版大幅度优化翻译措辞更地道添加了新译者笔记。Google 总部 C++ 工程师 innocentim, 清华大学不愿意透露姓名的唐马儒先生,大阪大学大学院情报科学研究科计算机科学专攻博士 farseerfc 和其它 Arch Linux 中文社区众帮了译者不少忙,谢谢他们。因为 C++ Primer 尚未完全入门,暂时没有翻译「类」章节和其它一些小章节。
- 2009-06 3.133 : YuleFox 的 1.0 版已经相当完善, 但原版在近一年的时间里, 其规范也发生了一些变化.
brantyoung 与 YuleFox 一拍即合, 以项目的形式来延续中文版 : `Google 开源项目风格指南 - 中文版项目 <http://github.com/brantyoung/zh-google-styleguide>`_.
@@ -51,7 +59,7 @@ Google 经常会发布一些开源项目, 意味着会接受来自其他代码
0.2 背景
---------------
-------
C++ 是 Google 大部分开源项目的主要编程语言. 正如每个 C++ 程序员都知道的, C++ 有很多强大的特性, 但这种强大不可避免的导致它走向复杂,使代码更容易产生 bug, 难以阅读和维护.

View File

@@ -1,28 +1,59 @@
4. 来自 Google 的奇技
------------------------
-------------------
Google 用了很多自己实现的技巧 / 工具使 C++ 代码更加健壮, 我们使用 C++ 的方式可能和你在其它地方见到的有所不同.
4.1. 智能指针
~~~~~~~~~~~~~~~~~~~~
4.1. 所有权与智能指针
~~~~~~~~~~~~~~~~~
.. tip::
如果确实需要使用智能指针的话, ``scoped_ptr`` 完全可以胜任. 你应该只在非常特定的情况下使用 ``std::tr1::shared_ptr``, 例如 STL 容器中的对象. 任何情况下都不要使用 ``auto_ptr``.
"智能" 指针看上去是指针, 其实是附加了语义的对象. 以 ``scoped_ptr`` 为例, ``scoped_ptr`` 被销毁时, 它会删除所指向的对象. ``shared_ptr`` 也是如此, 并且 ``shared_ptr`` 实现了引用计数, 所以最后一个 ``shared_ptr`` 对象析构时, 如果检测到引用次数为 0就会销毁所指向的对象.
动态分配出的对象最好有单一且固定的所有主onwer, 且通过智能指针传递所有权ownership.
一般来说,我们倾向于设计对象隶属明确的代码, 最明确的对象隶属是根本不使用指针, 直接将对象作为一个作用域或局部变量使用. 另一种极端做法是, 引用计数指针不属于任何对象. 这种方法的问题是容易导致循环引用, 或者导致某个对象无法删除的诡异状态, 而且在每一次拷贝或赋值时连原子操作都会很慢.
定义:
虽然不推荐使用引用计数指针, 但有些时候它们的确是最简单有效的解决方案.
所有权是一种登记/管理动态内存和其它资源的技术。动态分配出的对象的所有主是一个对象或函数,后者负责确保当前者无用时就自动销毁前者。所有权有时可以共享,那么就由最后一个所有主来负责销毁它。甚至也可以不用共享,在代码中直接把所有权传递给其它对象。
(YuleFox 注: 看来, Google 所谓的不同之处, 在于尽量避免使用智能指针 :D, 使用时也尽量局部化, 并且, 安全第一)
其实您可以把智能指针当成一个重载了 ``*````->`` 的「对象」来看。智能指针类型被用来自动化所有权的登记工作,来确保执行销毁义务到位。`std::unique_ptr <http://en.cppreference.com/w/cpp/memory/unique_ptr>`_ 是 C++11 新推出的一种智能指针类型,用来表示动态分配出的对象的「独一无二」所有权;当 ``std::unique_ptr`` 离开作用域,对象就会被销毁。不能复制 ``std::unique_ptr``, 但可以把它移动move给新所有主。`std::shared_ptr <http://en.cppreference.com/w/cpp/memory/shared_ptr>`_ 同样表示动态分配对象的所有权,但可以被共享,也可以被复制;对象的所有权由所有复制者共同拥有,最后一个复制者被销毁时,对象也会随着被销毁。
优点:
* 如果没有清晰、逻辑条理的所有权安排,不可能管理好动态分配的内存。
* 传递对象的所有权,开销比复制来得小,如果可以复制的话。
* 传递所有权也比「借用」指针或引用来得简单,毕竟它大大省去了两个用户一起协调对象生命周期的工作。
* 如果所有权逻辑条理,有文档且不乱来的话,可读性很棒。
* 可以不用手动完成所有权的登记工作,大大简化了代码,也免去了一大波错误之恼。
* 对于 const 对象来说,智能指针简单易用,也比深度复制高效。
缺点:
* 不得不用指针(不管是智能的还是原生的)来表示和传递所有权。指针语义可要比值语义复杂得许多了,特别是在 API 里您不光要操心所有权还要顾及别名生命周期可变性mutability以及其它大大小小问题。
* 其实值语义的开销经常被高估,所以就所有权的性能来说,可不能光只考虑可读性以及复杂性。
* 如果 API 依赖所有权的传递,就会害得客户端不得不用单一的内存管理模型。
* 销毁资源并回收的相关代码不是很明朗。
* ``std::unique_ptr`` 的所有权传递原理是 C++11 的 move 语法,后者毕竟是刚刚推出的,容易迷惑程序员。
* 如果原本的所有权设计已经够完善了,那么若要引入所有权共享机制,可能不得不重构整个系统。
* 所有权共享机制的登记工作在运行时进行,开销可能相当不小。
* 某些极端情况下所有权被共享的对象永远不会被销毁比如引用死循环cyclic references
* 智能指针并不能够完全代替原生指针。
4.2. cpplint
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~
.. tip::
使用 ``cpplint.py`` 检查风格错误.
``cpplint.py`` 是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报, 但它仍然是一个非常有用的工具. 用行注释 ``// NOLINT`` 可以忽略误报.
``cpplint.py`` 是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报, 但它仍然是一个非常有用的工具. 在行尾加 ``// NOLINT``, 或在上一行加 ``// NOLINTNEXTLINE``, 可以忽略报错。
某些项目会指导你如何使用他们的项目工具运行 ``cpplint.py``. 如果你参与的项目没有提供, 你可以单独下载 `cpplint.py <http://google-styleguide.googlecode.com/svn/trunk/cpplint/cpplint.py>`_.
译者acgtyrant笔记
~~~~~~~~~~~~~~~~~~~
#. 把智能指针当成对象来看待的话,就很好领会它与所指对象之间的关系了。
#. 原来 Rust 的 Ownership 思想是受到了 C++ 智能指针的很大启发啊。
#. ``scoped_ptr````auto_ptr`` 已过时。 现在是 ``shared_ptr````uniqued_ptr`` 的天下了。
#. 按本文来说,似乎除了智能指针,还有其它所有权机制,值得留意。
#. Arch Linux 用户注意了AUR 有对 cpplint 打包。

View File

@@ -1,169 +1,174 @@
6. 命名约定
------------
---------
最重要的一致性规则是命名管理. 命名风格快速获知名字代表是什么东东: 类型? 变量? 函数? 常量? 宏 ... ? 甚至不需要去查找类型声明. 我们大脑中的模式匹配引擎可以非常可靠的处理这些命名规则.
命名规则具有一定随意性, 但相比按个人喜好命名, 一致性更重, 所以不管你怎么想, 规则总归是规则.
6.1. 通用命名规则
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~
.. tip::
函数命名, 变量命名, 文件命名应具备描述性; 不要过度缩写. 类型和变量应该是名词, 函数名可以用 "命令性" 动词.
如何命名:
尽可能给描述性的名称. 不要节约行空间, 让别人很快理解你的代码更重要. 好的命名风格:
函数命名,变量命名,文件命名要有描述性;少用缩写。
尽可能给描述性的命名,别心疼空间,毕竟让代码易于新读者理解很重要。不要用只有项目开发者能理解的缩写,也不要通过砍掉几个字母来缩写单词。
.. code-block:: c++
int price_count_reader; // 无缩写
int num_errors; // “num” 本来就很常见
int num_dns_connections; // 人人都知道 “DNS” 是啥
.. warning::
.. code-block:: c++
int num_errors; // Good.
int num_completed_connections; // Good.
糟糕的命名使用含糊的缩写或随意的字符:
.. code-block:: c++
int n; // Bad - meaningless.
int nerr; // Bad - ambiguous abbreviation.
int n_comp_conns; // Bad - ambiguous abbreviation.
类型和变量名一般为名词: 如 ``FileOpener``, ``num_errors``.
函数名通常是指令性的 (确切的说它们应该是命令), 如 ``OpenFile()``, ``set_num_errors()``. 取值函数是个特例 (在 :ref:`函数命名 <function-names>` 处详细阐述), 函数名和它要取值的变量同名.
缩写:
除非该缩写在其它地方都非常普遍, 否则不要使用. 例如:
.. code-block:: c++
// Good
// These show proper names with no abbreviations.
int num_dns_connections; // 大部分人都知道 "DNS" 是啥意思.
int price_count_reader; // OK, price count. 有意义.
.. warning::
.. code-block:: c++
// Bad!
// Abbreviations can be confusing or ambiguous outside a small group.
int wgc_connections; // Only your group knows what this stands for.
int pc_reader; // Lots of things can be abbreviated "pc".
永远不要用省略字母的缩写:
.. code-block:: c++
int error_count; // Good.
int error_cnt; // Bad.
int n; // 莫名其妙。
int nerr; // 怪缩写。
int n_comp_conns; // 怪缩写。
int wgc_connections; // 只有贵团队知道是啥意思。
int pc_reader; // "pc" 有太多可能的解释了。
int cstmr_id; // 有删减若干字母。
6.2. 文件命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
文件名要全部小写, 可以包含下划线 (``_``) 或连字符 (``-``). 按项目约定来.
可接受的文件命名::
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
文件名要全部小写, 可以包含下划线 (``_``) 或连字符 (``-``). 按项目约定来. 如果并没有项目约定,"_" 更好。
C++ 文件要以 ``.cc`` 结尾, 头文件以 ``.h`` 结尾.
可接受的文件命名::
不要使用已经存在于 ``/usr/include`` 下的文件名 (yospaly 注: 即编译器搜索系统头文件的路径), 如 ``db.h``.
* my_useful_class.cc
* my-useful-class.cc
* myusefulclass.cc
* muusefulclass_test.cc // ``_unittest`` 和 ``_regtest`` 已弃用。
通常应尽量让文件名更加明确. ``http_server_logs.h`` 就比 ``logs.h`` 要好. 定义类时文件名一般成对出现, 如 ``foo_bar.h````foo_bar.cc``, 对应于类 ``FooBar``.
C++ 文件要以 ``.cc`` 结尾, 头文件以 ``.h`` 结尾. 专门插入文本的文件则以 ``.inc`` 结尾,参见:ref:`self-contained headers`
内联函数必须放在 ``.h`` 文件中. 如果内联函数比较短, 就直接放在 ``.h`` 中. 如果代码比较长, 可以放到以 ``-inl.h`` 结尾的文件中. 对于包含大量内联代码的类, 可以使用三个文件::
url_table.h // The class declaration.
url_table.cc // The class definition.
url_table-inl.h // Inline functions that include lots of code.
参考 :ref:`-inl.h 文件 <inl-files>` 一节.
不要使用已经存在于 ``/usr/include`` 下的文件名 (yospaly 注: 即编译器搜索系统头文件的路径), 如 ``db.h``.
通常应尽量让文件名更加明确. ``http_server_logs.h`` 就比 ``logs.h`` 要好. 定义类时文件名一般成对出现, 如 ``foo_bar.h````foo_bar.cc``, 对应于类 ``FooBar``.
内联函数必须放在 ``.h`` 文件中. 如果内联函数比较短, 就直接放在 ``.h`` 中.
6.3. 类型命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
类型名称的每个单词首字母均大写, 不包含下划线: ``MyExcitingClass``, ``MyExcitingEnum``.
所有类型命名 —— 类, 结构体, 类型定义 (``typedef``), 枚举 —— 均使用相同约定. 例如:
.. code-block:: c++
// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// typedefs
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
// enums
enum UrlTableErrors { ...
6.4. 变量命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
变量名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 如::
my_exciting_local_variable
my_exciting_member_variable_
变量名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用,如:: ``a_local_variable``, ``a_struct_data_member``, ``a_class_data_member_``.
普通变量命名:
举例::
string table_name; // OK - uses underscore.
string tablename; // OK - all lowercase.
string table_name; // - 用下划线。
string tablename; // - 全小写。
.. warning::
.. code-block:: c++
string tableName; // Bad - mixed case.
结构体变量:
结构体的数据成员可以和普通变量一样, 不用像类那样接下划线:
string tableName; // 差 - 混合大小写。
类数据成员:
不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样, 但要接下划线。
.. code-block:: c++
class TableInfo {
...
private:
string table_name_; // 可 - 尾后加下划线。
string tablename_; // 可。
static Pool<TableInfo>* pool_; // 可。
};
结构体变量:
不管是静态的还是非静态的,结构体数据成员都可以和普通变量一样, 不用像类那样接下划线:
.. code-block:: c++
struct UrlTableProperties {
string name;
int num_entries;
}
结构体与类的讨论参考 :ref:`结构体 vs. 类 <structs_vs_classes>` 一节.
全局变量:
对全局变量没有特别要求, 少用就好, 但如果你要用, 可以用 ``g_`` 或其它标志作为前缀, 以便更好的区分局部变量.
结构体与类的讨论参考 :ref:`结构体 vs. 类 <structs_vs_classes>` 一节.
全局变量:
对全局变量没有特别要求, 少用就好, 但如果你要用, 可以用 ``g_`` 或其它标志作为前缀, 以便更好的区分局部变量.
.. _constant-names:
6.5. 常量命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
在名称前加 ``k``: kDaysInAWeek.
所有编译时常量, 无论是局部的, 全局的还是类中的, 和其他变量稍微区别一下. ``k`` 后接大写字母开头的单词::
const int kDaysInAWeek = 7;
在全局或类里的常量名称前加 ``k``: kDaysInAWeek. 且除去开头的 ``k`` 之外每个单词开头字母均大写。
所有编译时常量, 无论是局部的, 全局的还是类中的, 和其他变量稍微区别一下. ``k`` 后接大写字母开头的单词:
.. code-block:: c++
const int kDaysInAWeek = 7;
这规则适用于编译时的局部作用域常量,不过要按变量规则来命名也可以。
.. _function-names:
6.6. 函数命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: ``MyExcitingFunction()``, ``MyExcitingMethod()``, ``my_exciting_member_variable()``, ``set_my_exciting_member_variable()``.
常规函数:
函数名的每个单词首字母大写, 没有下划线::
AddTableEntry()
DeleteUrl()
函数名的每个单词首字母大写, 没有下划线。
如果您的某函数出错时就要直接 crash, 那么就在函数名加上 OrDie. 但这函数本身必须集成在产品代码里,且平时也可能会出错。
.. code-block:: c++
AddTableEntry()
DeleteUrl()
OpenFileOrDie()
取值和设值函数:
取值和设值函数要与存取的变量名匹配. 这儿摘录一个类, ``num_entries_`` 是该类的实例变量:
取值Accessors和设值Mutators函数要与存取的变量名匹配. 这儿摘录一个类, ``num_entries_`` 是该类的实例变量:
.. code-block:: c++
class MyClass {
public:
...
@@ -173,26 +178,28 @@ C++ 文件要以 ``.cc`` 结尾, 头文件以 ``.h`` 结尾.
private:
int num_entries_;
};
其它非常短小的内联函数名也可以用小写字母, 例如. 如果你在循环中调用这样的函数甚至都不用缓存其返回值, 小写命名就可以接受.
6.7. 名字空间命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~
.. tip::
名字空间用小写字母命名, 并基于项目名称和目录结构: ``google_awesome_project``.
关于名字空间的讨论和如何命名, 参考 :ref:`名字空间 <namespaces>` 一节.
6.8. 枚举命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
枚举的命名应当和 :ref:`常量 <constant-names>`:ref:`宏 <macro-names>` 一致: ``kEnumName`` 或是 ``ENUM_NAME``.
单独的枚举值应该优先采用 :ref:`常量 <constant-names>` 的命名方式. 但 :ref:`宏 <macro-names>` 方式的命名也可以接受. 枚举名 ``UrlTableErrors`` (以及 ``AlternateUrlTableErrors``) 是类型, 所以要用大小写混合的方式.
.. code-block:: c++
enum UrlTableErrors {
kOK = 0,
kErrorOutOfMemory,
@@ -209,35 +216,41 @@ C++ 文件要以 ``.cc`` 结尾, 头文件以 ``.h`` 结尾.
.. _macro-names:
6.9. 宏命名
~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~
.. tip::
你并不打算 :ref:`使用宏 <preprocessor-macros>`, 对吧? 如果你一定要用, 像这样命名: ``MY_MACRO_THAT_SCARES_SMALL_CHILDREN``.
参考 `预处理宏 <preprocessor-macros>`; 通常 *不应该* 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线::
你并不打算:ref:`使用宏 <preprocessor-macros>`, 对吧? 如果你一定要用, 像这样命名: ``MY_MACRO_THAT_SCARES_SMALL_CHILDREN``.
参考:ref:`预处理宏 <preprocessor-macros>`; 通常 *不应该* 使用宏. 如果不得不用, 其命名像枚举命名一样全部大写, 使用下划线::
#define ROUND(x) ...
#define PI_ROUNDED 3.0
6.10. 命名规则的特例
~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~
.. tip::
如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略.
``bigopen()``:
函数名, 参照 ``open()`` 的形式
``uint``:
``typedef``
``bigpos``:
``struct````class``, 参照 ``pos`` 的形式
``sparse_hash_map``:
STL 相似实体; 参照 STL 命名约定
``LONGLONG_MAX``:
常量, 如同 ``INT_MAX``

File diff suppressed because it is too large Load Diff

View File

@@ -1,60 +1,80 @@
2. 作用域
-------------
--------
.. _namespaces:
2.1. 名字空间
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~
.. tip::
鼓励在 ``.cc`` 文件内使用匿名名字空间. 使用具名的名字空间时, 其名称可基于项目名或相对路径. 不要使用 *using 关键字*.
鼓励在 ``.cc`` 文件内使用匿名名字空间. 使用具名的名字空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示using-directive。禁止使用内联命名空间inline namespace
定义:
名字空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.
优点:
虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 名字空间在这基础上又封装了一层.
举例来说, 两个不同项目的全局作用域都有一个类 ``Foo``, 这样在编译或运行时造成冲突. 如果每个项目将代码置于不同名字空间中, ``project1::Foo````project2::Foo`` 作为不同符号自然不会冲突.
缺点:
名字空间具有迷惑性, 因为它们和类一样提供了额外的 (可嵌套的) 命名轴线.
内联命名空间会自动把内部的标识符放到外层作用域,比如:
.. code-block:: c++
namespace X {
inline namespace Y {
void foo();
}
}
``X::Y::foo()````X::foo()`` 彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。
缺点:
名字空间具有迷惑性, 因为它们和类一样提供了额外的 (可嵌套的) 命名轴线.
命名空间很容易令人迷惑,毕竟它们不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。
在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)).
结论:
根据下文将要提到的策略合理使用命名空间.
2.1.1. 匿名名字空间
^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^
-``.cc`` 文件中, 允许甚至鼓励使用匿名名字空间, 以避免运行时的命名冲突:
.. code-block:: c++
namespace { // .cc 文件中
// 名字空间的内容无需缩进
enum { kUNUSED, kEOF, kERROR }; // 经常使用的符号
bool AtEof() { return pos_ == kEOF; } // 使用本名字空间内的符号 EOF
} // namespace
然而, 与特定类关联的文件作用域声明在该类中被声明为类型, 静态数据成员或静态成员函数, 而不是匿名名字空间的成员. 如上例所示, 匿名空间结束时用注释 ``// namespace`` 标识.
然而, 与特定类关联的文件作用域声明在该类中被声明为类型, 静态数据成员或静态成员函数, 而不是匿名名字空间的成员. 如上例所示, 匿名空间结束时用注释 ``// namespace`` 标识.
- 不要在 ``.h`` 文件中使用匿名名字空间.
2.1.2. 具名的名字空间
^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^
具名的名字空间使用方式如下:
- 用名字空间把文件包含, `gflags <http://code.google.com/p/google-gflags/>`_ 的声明/定义, 以及类的前置声明以外的整个源文件封装起来, 以区别于其它名字空间:
.. code-block:: c++
// .h 文件
namespace mynamespace {
// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
@@ -62,139 +82,170 @@
void Foo();
};
} // namespace mynamespace
.. code-block:: c++
// .cc 文件
namespace mynamespace {
// 函数定义都置于命名空间中
void MyClass::Foo() {
}
} // namespace mynamespace
通常的 ``.cc`` 文件包含更多, 更复杂的细节, 比如引用其他名字空间的类等.
.. code-block:: c++
#include “a.h”
DEFINE_bool(someflag, false, dummy flag);
class C; // 全局名字空间中类 C 的前置声明
namespace a { class A; } // a::A 的前置声明
namespace b {
code for b // b 中的代码
} // namespace b
- 不要在名字空间 ``std`` 内声明任何东西, 包括标准库的类前置声明. 在 ``std`` 名字空间声明实体会导致不确定的问题, 比如不可移植. 声明标准库下的实体, 需要包含对应的头文件.
- 最好不要使用 *``using`` 关键字*, 以保证名字空间下的所有名称都可以正常使用.
- 最好不要使用 using 指示,以保证名字空间下的所有名称都可以正常使用.
.. code-block:: c++
// 禁止 —— 污染名字空间
using namespace foo;
-``.cc`` 文件, ``.h`` 文件的函数, 方法或类中, 可以使用 *``using`` 关键字*.
-``.cc`` 文件, ``.h`` 文件的函数, 方法或类中, 可以使用 using 声明。
.. code-block:: c++
// 允许: .cc 文件中
// .h 文件的话, 必须在函数, 方法或类的内部使用
using ::foo::bar;
-``.cc`` 文件, ``.h`` 文件的函数, 方法或类中, 允许使用名字空间别名.
.. code-block:: c++
// 允许: .cc 文件中
// .h 文件的话, 必须在函数, 方法或类的内部使用
namespace fbz = ::foo::bar::baz;
// 在 .h 文件里
namespace librarian {
//以下别名在所有包含了该头文件的文件中生效。
namespace pd_s = ::pipeline_diagnostics::sidetable;
inline void my_inline_function() {
// namespace alias local to a function (or method).
namespace fbz = ::foo::bar::baz;
...
}
} // namespace librarian
注意在 .h 文件的别名对包含了该头文件的所有人可见,所以在公共头文件(在项目外可用)以及它们递归包含的其它头文件里,不要用别名。毕竟原则上公共 API 要尽可能地精简。
- 禁止用内联命名空间
2.2. 嵌套类
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~
.. tip::
当公有嵌套类作为接口的一部分时, 虽然可以直接将他们保持在全局作用域中, 但将嵌套类的声明置于名字空间内是更好的选择.
当公有嵌套类作为接口的一部分时, 虽然可以直接将他们保持在全局作用域中, 但将嵌套类的声明置于:ref:`namespaces`内是更好的选择.
定义: 在一个类内部定义另一个类; 嵌套类也被称为 *成员类 (member class)*.
.. code-block:: c++
class Foo {
private:
// Bar是嵌套在Foo中的成员类
class Bar {
};
};
优点:
当嵌套 (或成员) 类只被外围类使用时非常有用; 把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类. 嵌套类可以在外围类中做前置声明, 然后在 ``.cc`` 文件中定义, 这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关.
缺点:
嵌套类只能在外围类的内部做前置声明. 因此, 任何使用了 ``Foo::Bar*`` 指针的头文件不得不包含类 ``Foo`` 的整个声明.
结论:
不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项.
2.3. 非成员函数、静态成员函数和全局函数
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. tip::
使用静态成员函数或名字空间内的非成员函数, 尽量不要用裸的全局函数.
优点:
某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在名字空间内可避免污染全局作用域.
缺点:
将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此.
结论:
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用命名空间.
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内.
如果你必须定义非成员函数, 又只是在 ``.cc`` 文件中使用它, 可使用匿名名字空间或 ``static`` 链接关键字 (如 ``static int Foo() {...}``) 限定其作用域.
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用:ref:`namespaces`
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内.
如果你必须定义非成员函数, 又只是在 ``.cc`` 文件中使用它, 可使用匿名:ref:`namespaces```static`` 链接关键字 (如 ``static int Foo() {...}``) 限定其作用域.
2.4. 局部变量
~~~~~~~~~~~~~~~
~~~~~~~~~~~
.. tip::
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:
.. code-block:: c++
int i;
i = f(); // 坏——初始化和声明分离
int j = g(); // 好——初始化时声明
vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);
vector<int> v = {1, 2}; // 好——v 一开始就初始化
注意, GCC 可正确实现了 ``for (int i = 0; i < 10; ++i)`` (``i`` 的作用域仅限 ``for`` 循环内), 所以其他 ``for`` 循环中可以重新使用 ``i``. 在 ``if````while`` 等语句中的作用域声明也是正确的, 如:
.. code-block:: c++
while (const char* p = strchr(str, /)) str = p + 1;
.. warning:: 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数.
.. code-block:: c++
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
@@ -202,32 +253,36 @@ C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的
}
在循环作用域外面声明这类变量要高效的多:
.. code-block:: c++
Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
2.5. 静态和全局变量
~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~
.. tip::
禁止使用 ``class`` 类型的静态或全局变量: 它们会导致很难发现的 bug 和不确定的构造和析构函数调用顺序.
静态生存周期的对象, 包括全局变量, 静态变量, 静态类成员变量, 以及函数静态变量, 都必须是原生数据类型 (POD : Plain Old Data): 只能是 `int`, `char`, `float`, 和 `void`, 以及 POD 类型的数组/结构体/指针. 永远不要使用函数返回值初始化静态变量; 不要在多线程代码中使用非 ``const`` 的静态变量.
禁止使用 ``class`` 类型的静态或全局变量:它们会导致难以发现的 bug 和不确定的构造和析构函数调用顺序。不过 ``constexpr`` 变量除外,毕竟它们又不涉及动态初始化或析构。
不幸的是, 静态变量的构造函数, 析构函数以及初始化操作的调用顺序在 C++ 标准中未明确定义, 甚至每次编译构建都有可能会发生变化, 从而导致难以发现的 bug. 比如, 结束程序时, 某个静态变量已经被析构了, 但代码还在跑 -- 其它线程很可能 -- 试图访问该变量, 直接导致崩溃.
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。
所以, 我们只允许 POD 类型的静态变量. 本条规则完全禁止 ``vector`` (使用 C 数组替代), ``string`` (使用 ``const char*``), 及其它以任意方式包含或指向类实例的东东, 成为静态变量. 出于同样的理由, 我们不允许用函数返回值来初始化静态变量.
静态变量的构造函数、析构函数和初始化的顺序在 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 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。
如果你确实需要一个 ``class` 类型的静态或全局变量, 可以考虑在 ``main()`` 函数或 ``pthread_once()`` 内初始化一个你永远不会回收的指针.
.. note:: yospaly 译注:
上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量.
上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量.
译者 (YuleFox) 笔记
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -237,3 +292,11 @@ C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的
#. 尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元;
#. 多线程中的全局变量 (含静态成员变量) 不要使用 ``class`` 类型 (含 STL 容器), 避免不明确行为导致的 bug.
#. 作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率.
译者acgtyrant笔记
~~~~~~~~~~~~~~~~~~~~~~~~
#. 注意「using 指示using-directive」和「using 声明using-declaration」的区别。
#. 匿名名字空间说白了就是文件作用域,就像 C static 声明的作用域一样,后者已经被 C++ 标准提倡弃用。
#. 局部变量在声明的同时进行显式值初始化比起隐式初始化再赋值的两步过程要高效同时也贯彻了计算机体系结构重要的概念「局部性locality」。
#. 注意别在循环犯大量构造和析构的低级错误。