资源描述
Google C++ 风格指南
中文版
目录
Google C++ 风格指南 1
中文版 1
0. 扉页 3
0.1 译者前言 3
0.2 背景 4
1. 头文件 5
1.1. #define 保护 5
1.2. 头文件依赖 5
1.3. 内联函数 6
1.4. -inl.h文件 6
1.5. 函数参数的顺序 6
1.6. #include 的路径及顺序 7
译者 (YuleFox) 笔记 7
2. 作用域 8
2.1. 名字空间 8
2.2. 嵌套类 11
2.3. 非成员函数, 静态成员函数, 和全局函数 12
2.4. 局部变量 12
2.5. 静态和全局变量 13
译者 (YuleFox) 笔记 13
3. 类 13
3.1. 构造函数的职责 14
3.2. 默认构造函数 14
3.3. 显式构造函数 15
3.4. 拷贝构造函数 15
3.5. 结构体 VS. 类 16
3.6. 继承 17
3.7. 多重继承 17
3.8. 接口 18
3.9. 运算符重载 18
3.10. 存取控制 19
3.11. 声明顺序 19
3.12. 编写简短函数 20
译者 (YuleFox) 笔记 20
4. 来自 Google 的奇技 20
4.1. 智能指针 20
4.2. cpplint 21
5. 其他 C++ 特性 21
5.1. 引用参数 21
5.2. 函数重载 22
5.3. 缺省参数 22
5.4. 变长数组和 alloca() 23
5.5. 友元 23
5.6. 异常 23
5.7. 运行时类型识别 24
5.8. 类型转换 25
5.9. 流 25
5.10. 前置自增和自减 26
5.11. const 的使用 27
5.12. 整型 28
5.13. 64 位下的可移植性 29
5.14. 预处理宏 30
5.15. 0 和 NULL 30
5.16. sizeof 31
5.17. Boost 库 31
6. 命名约定 32
6.1. 通用命名规则 32
6.2. 文件命名 33
6.3. 类型命名 33
6.4. 变量命名 34
6.5. 常量命名 34
6.6. 函数命名 35
6.7. 名字空间命名 35
6.8. 枚举命名 35
6.9. 宏命名 36
6.10. 命名规则的特例 36
7. 注释 37
7.1. 注释风格 37
7.2. 文件注释 37
7.3. 类注释 37
7.4. 函数注释 38
7.5. 变量注释 39
7.6. 实现注释 40
7.7. 标点, 拼写和语法 41
7.8. TODO 注释 41
译者 (YuleFox) 笔记 42
8. 格式 42
8.1. 行长度 42
8.2. 非 ASCII 字符 43
8.3. 空格还是制表位 43
8.4. 函数声明与定义 43
8.5. 函数调用 45
8.6. 条件语句 46
8.7. 循环和开关选择语句 47
8.8. 指针和引用表达式 48
8.9. 布尔表达式 49
8.10. 函数返回值 49
8.11. 变量及数组初始化 49
8.12. 预处理指令 50
8.13. 类格式 50
8.14. 初始化列表 51
8.15. 名字空间格式化 51
8.16. 水平留白 52
8.17. 垂直留白 54
译者 (YuleFox) 笔记 54
9. 规则特例 55
9.1. 现有不合规范的代码 55
9.2. Windows 代码 55
10. 结束语 56
0. 扉页
版本:
3.133
原作者:
Benjy Weinberger
Craig Silverstein
Gregory Eitzmann
Mark Mentovai
Tashana Landray
翻译:
YuleFox
yospaly
项目主页:
· Google Style Guide
· Google 开源项目风格指南 - 中文版
0.1 译者前言
Google 经常会发布一些开源项目, 意味着会接受来自其他代码贡献者的代码. 但是如果代码贡献者的编程风格与 Google 的不一致, 会给代码阅读者和其他代码提交这造成不小的困扰. Google 因此发布了这份自己的编程风格, 使所有提交代码的人都能获知 Google 的编程风格.
翻译初衷:
规则的作用就是避免混乱. 但规则本身一定要权威, 有说服力, 并且是理性的. 我们所见过的大部分编程规范, 其内容或不够严谨, 或阐述过于简单, 或带有一定的武断性.
Google 保持其一贯的严谨精神, 5 万汉字的指南涉及广泛, 论证严密. 我们翻译该系列指南的主因也正是其严谨性. 严谨意味着指南的价值不仅仅局限于它罗列出的规范, 更具参考意义的是它为了列出规范而做的谨慎权衡过程.
指南不仅列出你要怎么做, 还告诉你为什么要这么做, 哪些情况下可以不这么做, 以及如何权衡其利弊. 其他团队未必要完全遵照指南亦步亦趋, 如前面所说, 这份指南是 Google 根据自身实际情况打造的, 适用于其主导的开源项目. 其他团队可以参照该指南, 或从中汲取灵感, 建立适合自身实际情况的规范.
我们在翻译的过程中, 收获颇多. 希望本系列指南中文版对你同样能有所帮助.
我们翻译时也是尽力保持严谨, 但水平所限, bug 在所难免. 有任何意见或建议, 可与我们取得联系.
中文版和英文版一样, 使用 Artistic License/GPL 开源许可.
中文版修订历史:
· 2009-06 3.133 : YuleFox 的 1.0 版已经相当完善, 但原版在近一年的时间里, 其规范也发生了一些变化.
yospaly 与 YuleFox 一拍即合, 以项目的形式来延续中文版 : Google 开源项目风格指南 - 中文版项目.
主要变化是同步到 3.133 最新英文版本, 做部分勘误和改善可读性方面的修改, 并改进排版效果. yospaly 重新翻修, YuleFox 做后续评审.
· 2008-07 1.0 : 出自 YuleFox 的 Blog, 很多地方摘录的也是该版本.
0.2 背景
C++ 是 Google 大部分开源项目的主要编程语言. 正如每个 C++ 程序员都知道的, C++ 有很多强大的特性, 但这种强大不可避免的导致它走向复杂,使代码更容易产生 bug, 难以阅读和维护.
本指南的目的是通过详细阐述 C++ 注意事项来驾驭其复杂性. 这些规则在保证代码易于管理的同时, 高效使用 C++ 的语言特性.
风格, 亦被称作可读性, 也就是指导 C++ 编程的约定. 使用术语 “风格” 有些用词不当, 因为这些习惯远不止源代码文件格式化这么简单.
使代码易于管理的方法之一是加强代码一致性. 让任何程序员都可以快速读懂你的代码这点非常重要. 保持统一编程风格并遵守约定意味着可以很容易根据 “模式匹配” 规则来推断各种标识符的含义. 创建通用, 必需的习惯用语和模式可以使代码更容易理解. 在一些情况下可能有充分的理由改变某些编程风格, 但我们还是应该遵循一致性原则,尽量不这么做.
本指南的另一个观点是 C++ 特性的臃肿. C++ 是一门包含大量高级特性的庞大语言. 某些情况下, 我们会限制甚至禁止使用某些特性. 这么做是为了保持代码清爽, 避免这些特性可能导致的各种问题. 指南中列举了这类特性, 并解释为什么这些特性被限制使用.
Google 主导的开源项目均符合本指南的规定.
注意: 本指南并非 C++ 教程, 我们假定读者已经对 C++ 非常熟悉.
1. 头文件
通常每一个 .cc 文件都有一个对应的 .h 文件. 也有一些常见例外, 如单元测试代码和只包含 main() 函数的 .cc 文件.
正确使用头文件可令代码在可读性、文件大小和性能上大为改观.
下面的规则将引导你规避使用头文件时的各种陷阱.
1.1. #define 保护
小技巧
所有头文件都应该使用 #define 防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_
为保证唯一性, 头文件的命名应该依据所在项目源代码树的全路径. 例如, 项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
…
#endif // FOO_BAR_BAZ_H_
1.2. 头文件依赖
小技巧
能用前置声明的地方尽量不使用 #include.
当一个头文件被包含的同时也引入了新的依赖, 一旦该头文件被修改, 代码就会被重新编译. 如果这个头文件又包含了其他头文件, 这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译. 因此, 我们倾向于减少包含头文件, 尤其是在头文件中包含头文件.
使用前置声明可以显著减少需要包含的头文件数量. 举例说明: 如果头文件中用到类 File, 但不需要访问 File 类的声明, 头文件中只需前置声明 class File; 而无须 #include "file/base/file.h".
不允许访问类的定义的前提下, 我们在一个头文件中能对类 Foo 做哪些操作?
· 我们可以将数据成员类型声明为 Foo * 或 Foo &.
· 我们可以将函数参数 / 返回值的类型声明为 Foo (但不能定义实现).
· 我们可以将静态数据成员的类型声明为 Foo, 因为静态数据成员的定义在类定义之外.
反之, 如果你的类是 Foo 的子类, 或者含有类型为 Foo 的非静态数据成员, 则必须包含 Foo 所在的头文件.
有时, 使用指针成员 (如果是 scoped_ptr 更好) 替代对象成员的确是明智之选. 然而, 这会降低代码可读性及执行效率, 因此如果仅仅为了少包含头文件,还是不要这么做的好.
当然 .cc 文件无论如何都需要所使用类的定义部分, 自然也就会包含若干头文件.
1.3. 内联函数
小技巧
只有当函数只有 10 行甚至更少时才将其定义为内联函数.
定义:
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
优点:
当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
缺点:
滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
结论:
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).
有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.
1.4. -inl.h文件
小技巧
复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中.
内联函数的定义必须放在头文件中, 编译器才能在调用点内联展开定义. 然而, 实现代码理论上应该放在 .cc 文件中, 我们不希望 .h 文件中有太多实现代码, 除非在可读性和性能上有明显优势.
如果内联函数的定义比较短小, 逻辑比较简单, 实现代码放在 .h 文件里没有任何问题. 比如, 存取函数的实现理所当然都应该放在类定义内. 出于编写者和调用者的方便, 较复杂的内联函数也可以放到 .h 文件中, 如果你觉得这样会使头文件显得笨重, 也可以把它萃取到单独的 -inl.h 中. 这样把实现和类定义分离开来, 当需要时包含对应的 -inl.h 即可。
-inl.h 文件还可用于函数模板的定义. 从而增强模板定义的可读性.
别忘了 -inl.h 和其他头文件一样, 也需要 #define 保护.
1.5. 函数参数的顺序
小技巧
定义函数时, 参数顺序依次为: 输入参数, 然后是输出参数.
C/C++ 函数参数分为输入参数, 输出参数, 和输入/输出参数三种. 输入参数一般传值或传 const 引用, 输出参数或输入/输出参数则是非-const 指针. 对参数排序时, 将只输入的参数放在所有输出参数之前. 尤其是不要仅仅因为是新加的参数, 就把它放在最后; 即使是新加的只输入参数也要放在输出参数.
这条规则并不需要严格遵守. 输入/输出两用参数 (通常是类/结构体变量) 把事情变得复杂, 为保持和相关函数的一致性, 你有时不得不有所变通.
1.6. #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 文件
这种排序方式可有效减少隐藏依赖. 我们希望每一个头文件都是可被独立编译的 (yospaly 译注: 即该头文件本身已包含所有必要的显式依赖), 最简单的方法是将其作为第一个 .h 文件 #included 进对应的 .cc.
dir/foo.cc 和 dir2/foo2.h 通常位于同一目录下 (如 base/basictypes_unittest.cc 和 base/basictypes.h), 但也可以放在不同目录下.
按字母顺序对头文件包含进行二次排序是不错的主意 (yospaly 译注: 之前已经按头文件类别排过序了).
举例来说, google-awesome-project/src/foo/internal/fooserver.cc 的包含次序如下:
#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"
译者 (YuleFox) 笔记
1. 避免多重包含是学编程时最基本的要求;
2. 前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;
3. 内联函数的合理使用可提高代码执行效率;
4. -inl.h 可提高代码可读性 (一般用不到吧:D);
5. 标准化函数参数顺序可以提高可读性和易维护性 (对函数参数的堆栈空间有轻微影响, 我以前大多是相同类型放在一起);
6. 包含文件的名称使用 . 和 .. 虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理, 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在 “最需要编译” (对应源文件处 :D) 的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了.
2. 作用域
2.1. 名字空间
小技巧
鼓励在 .cc 文件内使用匿名名字空间. 使用具名的名字空间时, 其名称可基于项目名或相对路径. 不要使用using 关键字.
定义:
名字空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.
优点:
虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 名字空间在这基础上又封装了一层.
举例来说, 两个不同项目的全局作用域都有一个类 Foo, 这样在编译或运行时造成冲突. 如果每个项目将代码置于不同名字空间中,project1::Foo 和 project2::Foo 作为不同符号自然不会冲突.
缺点:
名字空间具有迷惑性, 因为它们和类一样提供了额外的 (可嵌套的) 命名轴线.
在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)).
结论:
根据下文将要提到的策略合理使用命名空间.
2.1.1. 匿名名字空间
· 在 .cc 文件中, 允许甚至鼓励使用匿名名字空间, 以避免运行时的命名冲突:
· namespace { // .cc 文件中
·
· // 名字空间的内容无需缩进
· enum { kUNUSED, kEOF, kERROR }; // 经常使用的符号
· bool AtEof() { return pos_ == kEOF; } // 使用本名字空间内的符号 EOF
·
· } // namespace
然而, 与特定类关联的文件作用域声明在该类中被声明为类型, 静态数据成员或静态成员函数, 而不是匿名名字空间的成员. 如上例所示, 匿名空间结束时用注释 // namespace 标识.
· 不要在 .h 文件中使用匿名名字空间.
2.1.2. 具名的名字空间
具名的名字空间使用方式如下:
· 用名字空间把文件包含, gflags 的声明/定义, 以及类的前置声明以外的整个源文件封装起来, 以区别于其它名字空间:
· // .h 文件
· namespace mynamespace {
·
· // 所有声明都置于命名空间中
· // 注意不要使用缩进
· class MyClass {
· public:
· …
· void Foo();
· };
·
} // namespace mynamespace
// .cc 文件
namespace mynamespace {
// 函数定义都置于命名空间中
void MyClass::Foo() {
…
}
} // namespace mynamespace
通常的 .cc 文件包含更多, 更复杂的细节, 比如引用其他名字空间的类等.
#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 namespace foo;
· 在 .cc 文件, .h 文件的函数, 方法或类中, 可以使用 ``using`` 关键字.
· // 允许: .cc 文件中
· // .h 文件的话, 必须在函数, 方法或类的内部使用
· using ::foo::bar;
· 在 .cc 文件, .h 文件的函数, 方法或类中, 允许使用名字空间别名.
· // 允许: .cc 文件中
· // .h 文件的话, 必须在函数, 方法或类的内部使用
·
· namespace fbz = ::foo::bar::baz;
2.2. 嵌套类
小技巧
当公有嵌套类作为接口的一部分时, 虽然可以直接将他们保持在全局作用域中, 但将嵌套类的声明置于名字空间内是更好的选择.
定义: 在一个类内部定义另一个类; 嵌套类也被称为 成员类 (member class).
class Foo {
private:
// Bar是嵌套在Foo中的成员类
class Bar {
…
};
};
优点:
当嵌套 (或成员) 类只被外围类使用时非常有用; 把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类. 嵌套类可以在外围类中做前置声明, 然后在 .cc 文件中定义, 这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关.
缺点:
嵌套类只能在外围类的内部做前置声明. 因此, 任何使用了 Foo::Bar* 指针的头文件不得不包含类 Foo 的整个声明.
结论:
不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项.
2.3. 非成员函数, 静态成员函数, 和全局函数
小技巧
使用静态成员函数或名字空间内的非成员函数, 尽量不要用裸的全局函数.
优点:
某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在名字空间内可避免污染全局作用域.
缺点:
将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此.
结论:
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用命名空间.
定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内.
如果你必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名名字空间或 static 链接关键字 (如 static int Foo() {...}) 限定其作用域.
2.4. 局部变量
小技巧
将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.
C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:
int i;
i = f(); // 坏——初始化和声明分离
nt j = g(); // 好——初始化时声明
注意, GCC 可正确实现了 for (int i = 0; i < 10; ++i) (i 的作用域仅限 for 循环内), 所以其他 for 循环中可以重新使用 i. 在 if 和while 等语句中的作用域声明也是正确的, 如:
while (const char* p = strchr(str, ‘/’)) str = p + 1;
警告
如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数.
// 低效的实现
for (int i = 0; i < 1000000; ++i) {
Foo f; // 构造函数和析构函数分别调用 1000000 次!
f.DoSomething(i);
}
在循环作用域外面声明这类变量要高效的多:
Foo f; // 构造函数和析构函数只调用 1 次
for (int i = 0; i < 1000000; ++i) {
f.DoSomething(i);
}
2.5. 静态和全局变量
小技巧
禁止使用 class 类型的静态或全局变量: 它们会导致很难发现的 bug 和不确定的构造和析构函数调用顺序.
静态生存周期的对象, 包括全局变量, 静态变量, 静态类成员变量, 以及函数静态变量, 都必须是原生数据类型 (POD : Plain Old Data): 只能是 int, char, float, 和 void, 以及 POD 类型的数组/结构体/指针. 永远不要使用函数返回值初始化静态变量; 不要在多线程代码中使用非const 的静态变量.
不幸的是, 静态变量的构造函数, 析构函数以及初始化操作的调用顺序在 C++ 标准中未明确定义, 甚至每次编译构建都有可能会发生变化, 从而导致难以发现的 bug. 比如, 结束程序时, 某个静态变量已经被析构了, 但代码还在跑 – 其它线程很可能 – 试图访问该变量, 直接导致崩溃.
所以, 我们只允许 POD 类型的静态变量. 本条规则完全禁止 vector (使用 C 数组替代), string (使用 const char*), 及其它以任意方式包含或指向类实例的东东, 成为静态变量. 出于同样的理由, 我们不允许用函数返回值来初始化静态变量.
如果你确实需要一个 class` 类型的静态或全局变量, 可以考虑在 ``main() 函数或 pthread_once() 内初始化一个你永远不会回收的指针.
注解
yospaly 译注:
上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量.
译者 (YuleFox) 笔记
1. cc 中的匿名名字空间可避免命名冲突, 限定作用域, 避免直接使用 using 关键字污染命名空间;
2. 嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要 public;
3. 尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元;
4. 多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致的 bug.
5. 作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率.
3. 类
类是 C++ 中代码的基本单元. 显然, 它们被广泛使用. 本节列举了在写一个类时的主要注意事项.
3.1. 构造函数的职责
小技巧
构造函数中只进行那些没什么意义的 (trivial, YuleFox 注: 简单初始化对于程序执行没有实际的逻辑意义, 因为成员变量 “有意义” 的值大多不在构造函数中确定) 初始化, 可能的话, 使用 Init() 方法集中初始化有意义的 (non-trivial) 数据.
定义:
在构造函数体中进行初始化操作.
优点:
排版方便, 无需担心类是否已经初始化.
缺点:
在构造函数中执行操作引起的问题有:
· 构造函数中很难上报错误, 不能使用异常.
· 操作失败会造成对象初始化失败,进入不确定状态.
· 如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患.
· 如果有人创建该类型的全局变量 (虽然违背了上节提到的规则), 构造函数将先 main() 一步被调用, 有可能破坏构造函数中暗含的假设条件. 例如, gflags 尚未初始化.
结论:
如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的 Init() 方法并 (或) 增加一个成员标记用于指示对象是否已经初始化成功.
3.2. 默认构造函数
小技巧
如果一个类定义了若干成员变量又没有其它构造函数, 必须定义一个默认构造函数. 否则编译器将自动生产一个很糟糕的默认构造函数.
定义:
new 一个不带参数的类对象时, 会调用这个类的默认构造函数. 用 new[] 创建数组时,默认构造函数则总是被调用.
优点:
默认将结构体初始化为 “无效” 值, 使调试更方便.
缺点:
对代码编写者来说, 这是多余的工作.
结论:
如果类中定义了成员变量, 而且没有提供其它构造函数, 你必须定义一个 (不带参数的) 默认构造函数. 把对象的内部状态初始化成一致/有效的值无疑是更合理的方式.
这么做的原因是: 如果你没有提供其它构造函数, 又没有定义默认构造函数, 编译器将为你自动生成一个. 编译器生成的构造函数并不会对对象进行合理的初始化.
如果你定义的类继承现有类, 而你又没有增加新的成员变量, 则不需要为新类定义默认构造函数.
3.3. 显式构造函数
小技巧
对单个参数的构造函数使用 C++ 关键字 explicit.
定义:
通常, 如果构造函数只有一个参数, 可看成是一种隐式转换. 打个比方, 如果你定义了 Foo::Foo(string name), 接着把一个字符串传给一个以 Foo 对象为参数的函数, 构造函数 Foo::Foo(string name) 将被调用, 并将该字符串转换为一个 Foo 的临时对象传给调用函数. 看上去很方便, 但如果你并不希望如此通过转换生成一个新对象的话, 麻烦也随之而来. 为避免构造函数被调用造成隐式转换, 可以将其声明为 explicit.
优点:
避免不合时宜的变换.
缺点:
无
结论:
所有单参数构造函数都必须是显式的. 在类定义中, 将关键字 explicit 加到单参数构造函数前: explicit Foo(string name);
例外: 在极少数情况下, 拷贝构造函数可以不声明成 explicit. 作为其它类的透明包装器的类也是特例之一. 类似的例外情况应在注释中明确说明.
3.4. 拷贝构造函数
小技巧
仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数; 大部分情况下都不需要, 此时应使用 DISALLOW_COPY_AND_ASSIGN.
定义:
拷贝构造函数在复制一个对象到新建对象时被调用 (特别是对象传值时).
优点:
拷贝构造函数使得拷贝对象更加容易. STL 容器要求所有内容可拷贝, 可赋值.
缺点:
C++ 中的隐式对象拷贝是很多性能问题和 bug 的根源. 拷贝构造函数降低了代码可读性, 相比传引用, 跟踪传值的对象更加困难, 对象修改的地方变得难以捉摸.
结论:
大部分类并不需要可拷贝, 也不需要一个拷贝构造函数或重载赋值运算符. 不幸的是, 如果你不主动声明它们, 编译器会为你自动生成, 而且是 public 的.
可以考虑在类的 private: 中添加拷贝构造函数和赋值操作的空实现, 只有声明, 没有定义. 由于这些空函数声明为 private, 当其他代码试图使用它们的时候, 编译器将报错. 方便起见, 我们可以使用 DISALLOW_COPY_AND_ASSIGN 宏:
// 禁止使用拷贝构造函数和 operator= 赋值操作的宏
// 应该类的 private: 中使用
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
在 class foo: 中:
class Foo {
public:
Foo(int f);
~Foo();
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};
如上所述, 绝大多数情况下都应使用 DISALLOW_COPY_AND_ASSIGN 宏. 如果类确实需要可拷贝, 应在该类的头文件中说明原由, 并合理的定义拷贝构造函数和赋值操作. 注意在 operator= 中检测自我赋值的情况 (yospaly 注: 即 operator= 接收的参数是该对象本身).
为了能作为 STL 容器的值, 你可能有使类可拷贝的冲动. 在大多数类似的情况下, 真正该做的是把对象的 指针 放到 STL 容器中. 可以考虑使用 std::tr1::shared_ptr.
3.5. 结构体 VS. 类
小技巧
仅当只有数据时使用 struct, 其它一概使用 class.
在 C++ 中 struct 和 class 关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便为定义的数据类型选择合适的关键字.
struct 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域 (field), 而非函数调用. 除了构造函数, 析构函数, Initialize(), Reset(), Validate() 外, 不能提供其它功能的函数.
如果需要更多的函数功能, class 更适合. 如果拿不准, 就用 class.
为了和 STL 保持一致, 对于仿函数 (functors) 和特性 (traits) 可以不用 class 而是使用 struct.
注意: 类和结构体的成员变量使用 不同的命名规则.
3.6. 继承
小技巧
使用组合 (composition, YuleFox 注: 这一点也是 GoF 在 <<Design Patterns>> 里反复强调的) 常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.
定义:
当子类继承基类时, 子类包含了父基类所有数据及操作的定义. C++ 实践中, 继承主要用于两种场合: 实现继承 (implementation inheritance), 子类继承父类的实现代码; 接口继承 (interface inheritance), 子类仅继承父类的方法名称.
优点:
实现继承通过原封不动的复用基类代码减少了代码量. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误. 从编程角度而言, 接口继承是用来强制类输出特定的 API. 在类没有实现 API 中某个必须的方法时, 编译器同样会发现并报告错误.
缺点:
对于实现继承, 由
展开阅读全文