Google C++编程风格整理(一)

来源:互联网 发布:软件测试就业培训班 编辑:程序博客网 时间:2024/05/16 06:09

前言
C++ 是Google 大部分开源项目的主要编程语言;正如每个C++ 程序员都知道的,C++ 有很多强大的特性,但这种强大不可避免的导致它走向复杂,使代码更容易产生 bug,难以阅读和维护。

本指南的目的是通过详细阐述 C++ 注意事项来驾驭其复杂性. 这些规则在保证代码易于管理的同时, 高效使用 C++ 的语言特性.

风格, 亦被称作可读性, 也就是指导 C++ 编程的约定. 使用术语 “风格” 有些用词不当, 因为这些习惯远不止源代码文件格式化这么简单.

使代码易于管理的方法之一是加强代码一致性. 让任何程序员都可以快速读懂你的代码这点非常重要. 保持统一编程风格并遵守约定意味着可以很容易根据 “模式匹配” 规则来推断各种标识符的含义. 创建通用, 必需的习惯用语和模式可以使代码更容易理解. 在一些情况下可能有充分的理由改变某些编程风格, 但我们还是应该遵循一致性原则,尽量不这么做.

本指南的另一个观点是 C++ 特性的臃肿. C++ 是一门包含大量高级特性的庞大语言. 某些情况下, 我们会限制甚至禁止使用某些特性. 这么做是为了保持代码清爽, 避免这些特性可能导致的各种问题. 指南中列举了这类特性, 并解释为什么这些特性被限制使用.

Google 主导的开源项目均符合本指南的规定.
注意: 本指南并非 C++ 教程, 我们假定读者已经对 C++ 非常熟悉.

1.头文件
通常每一个 .cc 文件都有一个对应的 .h 文件. 也有一些常见例外, 如单元测试代码和只包含 main() 函数的 .cc 文件.正确使用头文件可令代码在可读性、文件大小和性能上大为改观.

下面的规则将引导你规避使用头文件时的各种陷阱.
1.1. #define 保护

Tip所有头文件都应该使用#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. 头文件依赖

Tip能用前置声明的地方尽量不使用 #include.

当一个头文件被包含的同时也引入了新的依赖, 一旦该头文件被修改, 代码就会被重新编译. 如果这个头文件又包含了其他头文件, 这些头文件的任何改变都将导致所有包含了该头文件的代码被重新编译. 因此, 我们倾向于减少包含头文件, 尤其是在头文件中包含头文件.
使用前置声明可以显著减少需要包含的头文件数量. 举例说明: 如果头文件中用到类 File, 但不需要访问 File 类的声明, 头文件中只需前置声明 class File; 而无须 #include “file/base/file.h”.

不允许访问类的定义的前提下, 我们在一个头文件中能对类 Foo 做哪些操作?
•我们可以将数据成员类型声明为 Foo * 或 Foo &.
•我们可以将函数参数 / 返回值的类型声明为 Foo (但不能定义实现).
•我们可以将静态数据成员的类型声明为 Foo, 因为静态数据成员的定义在类定义之外.

反之, 如果你的类是 Foo 的子类, 或者含有类型为 Foo 的非静态数据成员, 则必须包含 Foo 所在的头文件.
有时, 使用指针成员 (如果是 scoped_ptr 更好) 替代对象成员的确是明智之选. 然而, 这会降低代码可读性及执行效率, 因此如果仅仅为了少包含头文件,还是不要这么做的好.
当然 .cc 文件无论如何都需要所使用类的定义部分, 自然也就会包含若干头文件.

1.3. 内联函数

Tip只有当函数只有10行甚至更少时才将其定义为内联函数.

定义:
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.

优点:
当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.

缺点:
滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。

结论:
一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!

另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).

有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.

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 指针. 对参数排序时, 将只输入的参数放在所有输出参数之前. 尤其是不要仅仅因为是新加的参数, 就把它放在最后; 即使是新加的只输入参数也要放在输出参数.

这条规则并不需要严格遵守. 输入/输出两用参数 (通常是类/结构体变量) 把事情变得复杂, 为保持和相关函数的一致性, 你有时不得不有所变通.

1.6. #include 的路径及顺序

Tip使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: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"

笔记
1.避免多重包含是学编程时最基本的要求;

2.前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;

3.内联函数的合理使用可提高代码执行效率;

4.-inl.h 可提高代码可读性 (一般用不到吧:D);

5.标准化函数参数顺序可以提高可读性和易维护性 (对函数参数的堆栈空间有轻微影响, 我以前大多是相同类型放在一起);

6.包含文件的名称使用 . 和 .. 虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理, 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在 “最需要编译” (对应源文件处 :D) 的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了.

2.作用域
2.1. 名字空间

Tip鼓励在 .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. 嵌套类

Tip当公有嵌套类作为接口的一部分时, 虽然可以直接将他们保持在全局作用域中, 但将嵌套类的声明置于名字空间内是更好的选择.

定义: 在一个类内部定义另一个类; 嵌套类也被称为 成员类 (member class).

class Foo {private:    // Bar是嵌套在Foo中的成员类    class Bar {        …    };};

优点:
当嵌套 (或成员) 类只被外围类使用时非常有用; 把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类. 嵌套类可以在外围类中做前置声明, 然后在 .cc 文件中定义, 这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关.

缺点:
嵌套类只能在外围类的内部做前置声明. 因此, 任何使用了 Foo::Bar* 指针的头文件不得不包含类 Foo 的整个声明.

结论:
不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项.

2.3. 非成员函数, 静态成员函数, 和全局函数

Tip使用静态成员函数或名字空间内的非成员函数, 尽量不要用裸的全局函数.

优点:
某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在名字空间内可避免污染全局作用域.

缺点:
将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此.

结论:
有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用命名空间.

定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内.

如果你必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名名字空间或 static 链接关键字 (如 static int Foo() {…}) 限定其作用域.

2.4. 局部变量

Tip将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.

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;

Warning
如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数.

// 低效的实现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. 静态和全局变量

Tip禁止使用 class 类型的静态或全局变量: 它们会导致很难发现的 bug 和不确定的构造和析构函数调用顺序.

静态生存周期的对象, 包括全局变量, 静态变量, 静态类成员变量, 以及函数静态变量, 都必须是原生数据类型 (POD : Plain Old Data): 只能是 int, char, float, 和 void, 以及 POD 类型的数组/结构体/指针. 永远不要使用函数返回值初始化静态变量; 不要在多线程代码中使用非 const 的静态变量.

不幸的是, 静态变量的构造函数, 析构函数以及初始化操作的调用顺序在 C++ 标准中未明确定义, 甚至每次编译构建都有可能会发生变化, 从而导致难以发现的 bug. 比如, 结束程序时, 某个静态变量已经被析构了, 但代码还在跑 – 其它线程很可能 – 试图访问该变量, 直接导致崩溃.

所以, 我们只允许 POD 类型的静态变量. 本条规则完全禁止 vector (使用 C 数组替代), string (使用 const char*), 及其它以任意方式包含或指向类实例的东东, 成为静态变量. 出于同样的理由, 我们不允许用函数返回值来初始化静态变量.

如果你确实需要一个 class 类型的静态或全局变量, 可以考虑在 main() 函数或 pthread_once() 内初始化一个你永远不会回收的指针.

上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量.

笔记
1.cc 中的匿名名字空间可避免命名冲突, 限定作用域, 避免直接使用 using 关键字污染命名空间;

2.嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要 public;

3.尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元;

4.多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致的 bug;

5.作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率.

3.类
类是 C++ 中代码的基本单元. 显然, 它们被广泛使用. 本节列举了在写一个类时的主要注意事项.

3.1. 构造函数的职责

Tip构造函数中只进行那些没什么意义的 (trivial, YuleFox 注: 简单初始化对于程序执行没有实际的逻辑意义, 因为成员变量 “有意义” 的值大多不在构造函数中确定) 初始化, 可能的话, 使用 Init() 方法集中初始化有意义的 (non-trivial) 数据.

定义:
在构造函数体中进行初始化操作.

优点:
排版方便, 无需担心类是否已经初始化.

缺点:
在构造函数中执行操作引起的问题有:
•构造函数中很难上报错误, 不能使用异常.
•操作失败会造成对象初始化失败,进入不确定状态.
•如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患.
•如果有人创建该类型的全局变量 (虽然违背了上节提到的规则), 构造函数将先 main() 一步被调用, 有可能破坏构造函数中暗含的假设条件. 例如, gflags 尚未初始化.

结论:
如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的 Init() 方法并 (或) 增加一个成员标记用于指示对象是否已经初始化成功.

3.2. 默认构造函数

Tip如果一个类定义了若干成员变量又没有其它构造函数, 必须定义一个默认构造函数. 否则编译器将自动生产一个很糟糕的默认构造函数.

定义:
new 一个不带参数的类对象时, 会调用这个类的默认构造函数. 用 new[] 创建数组时,默认构造函数则总是被调用.

优点:
默认将结构体初始化为 “无效” 值, 使调试更方便.

缺点:
对代码编写者来说, 这是多余的工作.

结论:
如果类中定义了成员变量, 而且没有提供其它构造函数, 你必须定义一个 (不带参数的) 默认构造函数. 把对象的内部状态初始化成一致/有效的值无疑是更合理的方式.

这么做的原因是: 如果你没有提供其它构造函数, 又没有定义默认构造函数, 编译器将为你自动生成一个. 编译器生成的构造函数并不会对对象进行合理的初始化.如果你定义的类继承现有类, 而你又没有增加新的成员变量, 则不需要为新类定义默认构造函数.

3.3. 显式构造函数

Tip对单个参数的构造函数使用 C++ 关键字 explicit.

定义:
通常, 如果构造函数只有一个参数, 可看成是一种隐式转换. 打个比方, 如果你定义了 Foo::Foo(string name), 接着把一个字符串传给一个以 Foo 对象为参数的函数, 构造函数 Foo::Foo(string name) 将被调用, 并将该字符串转换为一个 Foo 的临时对象传给调用函数. 看上去很方便, 但如果你并不希望如此通过转换生成一个新对象的话, 麻烦也随之而来. 为避免构造函数被调用造成隐式转换, 可以将其声明为 explicit.

优点:
避免不合时宜的变换.

缺点:

结论:
所有单参数构造函数都必须是显式的. 在类定义中, 将关键字 explicit 加到单参数构造函数前: explicit Foo(string name);

例外: 在极少数情况下, 拷贝构造函数可以不声明成 explicit. 作为其它类的透明包装器的类也是特例之一. 类似的例外情况应在注释中明确说明.

3.4. 拷贝构造函数

Tip仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数; 大部分情况下都不需要, 此时应使用 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. 类

Tip仅当只有数据时使用 struct, 其它一概使用 class.

在 C++ 中 struct 和 class 关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便为定义的数据类型选择合适的关键字.

struct 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域 (field), 而非函数调用. 除了构造函数, 析构函数, Initialize(), Reset(), Validate() 外, 不能提供其它功能的函数.

如果需要更多的函数功能, class 更适合. 如果拿不准, 就用 class.

为了和 STL 保持一致, 对于仿函数 (functors) 和特性 (traits) 可以不用 class 而是使用 struct.

注意: 类和结构体的成员变量使用 不同的命名规则.

3.6. 继承

Tip使用组合 (composition, YuleFox 注: 这一点也是 GoF 在 <<Design Patterns>> 里反复强调的) 常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.

定义:
当子类继承基类时, 子类包含了父基类所有数据及操作的定义. C++ 实践中, 继承主要用于两种场合: 实现继承 (implementation inheritance), 子类继承父类的实现代码; 接口继承 (interface inheritance), 子类仅继承父类的方法名称.

优点:
实现继承通过原封不动的复用基类代码减少了代码量. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误. 从编程角度而言, 接口继承是用来强制类输出特定的 API. 在类没有实现 API 中某个必须的方法时, 编译器同样会发现并报告错误.

缺点:
对于实现继承, 由于子类的实现代码散布在父类和子类间之间, 要理解其实现变得更加困难. 子类不能重写父类的非虚函数, 当然也就不能修改其实现. 基类也可能定义了一些数据成员, 还要区分基类的实际布局.

结论:
所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式.

不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 “是一个” (“is-a”, YuleFox 注: 其他 “has-a” 情况下请使用组合) 的情况下使用继承: 如果 Bar 的确 “是一种” Foo, Bar 才能继承 Foo.

必要的话, 析构函数声明为 virtual. 如果你的类有虚函数, 则析构函数也应该为虚函数. 注意 数据成员在任何情况下都必须是私有的.

当重载一个虚函数, 在衍生类中把它明确的声明为 virtual. 理论依据: 如果省略 virtual 关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数.

3.7. 多重继承

Tip真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的 纯接口类.

定义:
多重继承允许子类拥有多个基类. 要将作为 纯接口 的基类和具有 实现 的基类区别开来.

优点:
相比单继承 (见 继承), 多重实现继承可以复用更多的代码.

缺点:
真正需要用到多重 实现 继承的情况少之又少. 多重实现继承看上去是不错的解决方案, 但你通常也可以找到一个更明确, 更清晰的不同解决方案.

结论:
只有当所有父类除第一个外都是 纯接口类 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以 Interface 为后缀.

3.8. 接口

Tip接口是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制).

定义:
当一个类满足以下要求时, 称之为纯接口:
•只有纯虚函数 (“=0“) 和静态函数 (除了下文提到的析构函数).
•没有非静态数据成员.
•没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 protected.
•如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.

接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数 (作为上述第 1 条规则的特例, 析构函数不能是纯虚函数). 具体细节可参考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 节.

优点:
以 Interface 为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员. 这一点对于 多重继承 尤其重要. 另外, 对于 Java 程序员来说, 接口的概念已是深入人心.

缺点:
Interface 后缀增加了类名长度, 为阅读和理解带来不便. 同时,接口特性作为实现细节不应暴露给用户.

结论:
只有在满足上述需要时, 类才以 Interface 结尾, 但反过来, 满足上述需要的类未必一定以 Interface 结尾.

3.9. 运算符重载

Tip除少数特定环境外,不要重载运算符.

定义:
一个类可以定义诸如 + 和 / 等运算符, 使其可以像内建类型一样直接操作.

优点:
使代码看上去更加直观, 类表现的和内建类型 (如 int) 行为一致. 重载运算符使 Equals(), Add() 等函数名黯然失色. 为了使一些模板函数正确工作, 你可能必须定义操作符.

缺点:
虽然操作符重载令代码更加直观, 但也有一些不足:
•混淆视听, 让你误以为一些耗时的操作和操作内建类型一样轻巧.
•更难定位重载运算符的调用点, 查找 Equals() 显然比对应的 == 调用点要容易的多.
•有的运算符可以对指针进行操作, 容易导致 bug. Foo + 4 做的是一件事, 而 &Foo + 4 可能做的是完全不同的另一件事. 对于二者, 编译器都不会报错, 使其很难调试;
重载还有令你吃惊的副作用. 比如, 重载了 operator& 的类不能被前置声明.

结论:
一般不要重载运算符. 尤其是赋值操作 (operator=) 比较诡异, 应避免重载. 如果需要的话, 可以定义类似 Equals(), CopyFrom() 等函数.

然而, 极少数情况下可能需要重载运算符以便与模板或 “标准” C++ 类互操作 (如 operator<<(ostream&, const T&)). 只有被证明是完全合理的才能重载, 但你还是要尽可能避免这样做. 尤其是不要仅仅为了在 STL 容器中用作键值就重载 operator== 或 operator<; 相反, 你应该在声明容器的时候, 创建相等判断和大小比较的仿函数类型.

有些 STL 算法确实需要重载 operator== 时, 你可以这么做, 记得别忘了在文档中说明原因.
参考 拷贝构造函数 和 函数重载.

3.10. 存取控制

Tip将 所有 数据成员声明为 private, 并根据需要提供相应的存取函数. 例如, 某个名为 foo_ 的变量, 其取值函数是 foo(). 还可能需要一个赋值函数 set_foo().

一般在头文件中把存取函数定义成内联函数.
参考 继承 和 函数命名

3.11. 声明顺序

Tip在类中使用特定的声明顺序: public: 在 private: 之前, 成员函数在数据成员 (变量) 前;

类的访问控制区段的声明顺序依次为: public:, protected:, private:. 如果某区段没内容, 可以不声明.

每个区段内的声明通常按以下顺序:
•typedefs 和枚举
•常量
•构造函数
•析构函数
•成员函数, 含静态成员函数
•数据成员, 含静态数据成员

宏 DISALLOW_COPY_AND_ASSIGN 的调用放在 private: 区段的末尾. 它通常是类的最后部分.
.cc 文件中函数的定义应尽可能和声明顺序一致.

不要在类定义中内联大型函数. 通常, 只有那些没有特别意义或性能要求高, 并且是比较短小的函数才能被定义为内联函数. 更多细节参考 内联函数.

3.12. 编写简短函数

Tip倾向编写简短, 凝练的函数.

我们承认长函数有时是合理的, 因此并不硬性限制函数的长度. 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.

即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题. 甚至导致难以发现的 bug. 使函数尽量简短, 便于他人阅读和修改代码.

在处理代码时, 你可能会发现复杂的长函数. 不要害怕修改现有代码: 如果证实这些代码使用 / 调试困难, 或者你需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数.

笔记
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. 智能指针

Tip如果确实需要使用智能指针的话, scoped_ptr 完全可以胜任. 你应该只在非常特定的情况下使用 std::tr1::shared_ptr, 例如 STL 容器中的对象. 任何情况下都不要使用 auto_ptr.

“智能” 指针看上去是指针, 其实是附加了语义的对象. 以 scoped_ptr 为例, scoped_ptr 被销毁时, 它会删除所指向的对象. shared_ptr 也是如此, 并且 shared_ptr 实现了引用计数, 所以最后一个 shared_ptr 对象析构时, 如果检测到引用次数为 0,就会销毁所指向的对象.

一般来说,我们倾向于设计对象隶属明确的代码, 最明确的对象隶属是根本不使用指针, 直接将对象作为一个作用域或局部变量使用. 另一种极端做法是, 引用计数指针不属于任何对象. 这种方法的问题是容易导致循环引用, 或者导致某个对象无法删除的诡异状态, 而且在每一次拷贝或赋值时连原子操作都会很慢.

虽然不推荐使用引用计数指针, 但有些时候它们的确是最简单有效的解决方案.
(YuleFox 注: 看来, Google 所谓的不同之处, 在于尽量避免使用智能指针 :D, 使用时也尽量局部化, 并且, 安全第一)

4.2. cpplint

Tip使用 cpplint.py 检查风格错误.

cpplint.py 是一个用来分析源文件, 能检查出多种风格错误的工具. 它不并完美, 甚至还会漏报和误报, 但它仍然是一个非常有用的工具. 用行注释 // NOLINT 可以忽略误报.

某些项目会指导你如何使用他们的项目工具运行 cpplint.py. 如果你参与的项目没有提供, 你可以单独下载 cpplint.py.

0 0