3 类 Classes
类是C++中代码的基本单元; 显然, 它们被广泛使用; 本节列举了写一个类时的主要注意事项;
3.1 构造函数的职责 Doing Work in Constructors
Tip 构造函数中只进行那些没什么意义的(trivial 译注: 简单初始化对于程序执行没有实际的逻辑意义, 因为成员变量"有意义"的值大多不再构造函数中确定)初始化, 可能的话, 使用 Init()方法集中初始化有意义的(non-trival)数据;
[Add] 在ctor中防止做复杂的初始化(特别是可能导致失败的动作, 或者需要vitrual method的调用; <<<
定义: 在构造函数体中进行初始化操作;
优点: 排版方便, 无需担心类是否已经初始化;
缺点:
在构造函数中执行操作引起的问题有:
- 构造函数中很难上报错误, 不能使用异常; [可以使用函数范围的异常机制 CLS::CLS try{} catch{}]
- 操作失败会造成对象初始化失败, 进入不确定状态;
- 如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向dispatched到子类的虚函数实现; 即使当前没有子类化实现, 将来仍是隐患; [考虑构造的子类-基类相对顺序, 基类构造时子类还未构造, 析构反过来也一样]
- 如果有人创建该类型的全局变量(虽然违背了上节提到的规则), 构造函数将先 main()一步被调用, 有可能破坏构造函数中暗含的假设条件; 例如: gflags尚未初始化;
结论: 如果对象需要进行有意义的(non-trivial)初始化, 考虑使用明确的 Init()方法/factory函数并(或)增加一个成员标记用于指示对象是否已经初始化成功;
[Add] 构造永远不该调用virtual函数, 否则会产生一些非致命的错误non-fatal failures; <<<
3.2 默认构造函数
Tip 如果一个类定义了若干成员变量又没有其他构造函数, 必须定义一个默认构造函数; 否则编译器将自动产生一个糟糕的默认构造函数;
定义: new一个不带icansh的类对象时, 会调用这个类的默认构造函数; 用 new[]创建数组时, 默认构造函数则总是被调用;
优点: 默认将结构体初始化为"无效"值, 使调试更方便;
缺点: 对代码编写者来说, 这是多余的工作;
结论:
如果类中定义了成员变量, 而且没有提供其他构造函数, 必须定义一个(不带参数的)默认构造函数; 把对象的内部状态初始化成一致/有效的值, 这无疑是更合理的方式;
这么做的原因是: 如果没有提供其他构造函数, 又没有定义默认构造函数, 编译器将自动生成一个; 编译器生成的构造函数并不会对对象进行合理的初始化;
如果你定义的类继承现有类, 而你又没有增加新的成员变量, 则不需要为新类定义默认构造函数;
[Add]
初始化 Initialization
如果你的类定义了成员变量, 必须提供一个in-class的initializer, 为每个成员变量进行初始化, 或者写一个ctor(作为默认构造); 如果你自己没有声明任何构造, 编译器会为你生成一个默认构造函数, 它会把很多域留在未初始化的状态, 或者初始化成一个不合适的值;
定义:
默认构造函数会在new一个没有参数的class时被调用; 在调用 new[](数组)操作是总是会调用它; in-class的成员初始化意味着: 使用一个构造如 int count_ = 17; 或 string name_{"abc"}来声明一个成员, 而不是仅仅 int count_; 或 string name_;
[c++11 http://stackoverflow.com/questions/13662441/c11-allows-in-class-initialization-of-non-static-and-non-const-members-what-c 类似Java的类内初始化]
优点:
如果没有提供intitializer, 一个用户定义的默认ctor会被用来初始化对象; 它可以保证对象一旦被构造, 总是在合法valid和usable可用的状态; 它还可以保证对象一开始创建的时候是一个明显的"不可用impossible"的状态, 有助于debugging;
缺点:
显式地定义一个默认构造对于程序员来说是多余的工作;
in-class成员初始化initialization可能会产生混淆, 如果一个成员变量在initialization中的部分内初始化, 然后又在构造函数中初始化, Note ctor中的值会覆盖声明时候的值, 这一点需要注意;
结论:
使用 in-class成员initialization来做简单的初始化, 特别当一个成员变量必须在多个ctor中做相同的初始化时;
如果你定义的成员变量没有在in-class中被初始化, 而且也没有其他的ctor, 你必须定义一个默认ctor(没有参数的); 它可以更好地preferably将对象初始化, 保持它的内部状态是一致的和合法的;
之所以使用initialization是因为如果你没有其他的ctor, 也没有定义一个默认ctor, 编译器会自动生成一个; 这个编译器生成的ctor为你所做的初始化可能是不合理的(not sensibly);
如果你的类继承自一个已存的类, 但是你添加了两个新的成员变量, 你可能就不一定要写一个默认构造了; [使用Initializer]
<<<
3.3 显式构造函数 Explicit Constructors
Tip 对单个参数的构造函数使用C++关键字 explicit;
定义:
通常, 如果构造函数只有一个参数, 可看成是一种隐式转换conversion; 举例, 如果定义了 Foo::Foo(string name), 接着把一个字符串传给一个以Foo对象为参数的函数, 构造函数 Foo::Foo(string name)将被调用, 并将该字符串转换为一个 Foo的临时对象传给调用函数; 这看似方便, 但如果你并不希望如此通过转换生成一个新对象的话, 麻烦也随之而来; 为避免构造函数被调用造成隐式转换, 可以将其声明为 explicit来防止这种隐式转换;
优点: 避免不合时宜的变换;
缺点: 无;
结论:
所有单参数构造函数都必须是显式的; 在类定义中, 将关键字 explicit加到单参数构造函数前: explicit Foo(string name);
例外: 在极少数情况下, 拷贝构造函数可以不声明成 explicit; 作为其他类的透明包装器的类也是特例之一; 类似的例外情况应在注释中明确说明;
[Add] copy和move构造是特例: 他们不应该是explicit的, 为了透明地transparent包装另一些类的class也是特例; 这些特例应该被明显地用comment解释并标注
[c++11 && http://msdn.microsoft.com/zh-cn/library/dd293665.aspx ]
最好, 对于仅仅接受一个 std::initialize_list的ctor可以是non-explicit的; 这允许你类型是通过 braced initializer list构造的, 就像是一个assignment-style的初始化, 函数参数, 或返回语句; 例如:
1 2 3 |
|
[c++11 {} http://www.cplusplus.com/reference/initializer_list/initializer_list/ ]
<<<
[Add]
可拷贝和可移动的类型 Copyable and Movable Types
如果你的类型需要, 可以支持拷贝, 移动; 否则, 要禁止隐式地产生特殊的方法来拷贝和移动;
定义:
一个可拷贝的类型允许它的对象通过另一个同类型的对象被初始化或赋值, 无需改变源对象的值; 对于用户定义的类型, 拷贝行为是在拷贝构造和拷贝赋值操作符中被定义的; string是一个可拷贝的类型;
一个可移动的类型可以通过临时值temporaries(所有可拷贝的类型是可移动的)来初始化和赋值; std::unique_ptr<int>是可移动但不可拷贝的类型; 对于用户定义的类型, 移动希望是定义在 move ctor和 move-assignment操作符中的;
[c++11 move && http://en.cppreference.com/w/cpp/language/move_constructor http://en.cppreference.com/w/cpp/language/move_operator ]
copy/move ctor在某些情况下可以被编译器隐式地调用, e.g. 按值传递对象时;
优点:
可拷贝和可移动的类型的对象可以在return时按值传递, 让API保持简单, 安全和更通用; 不像传递指针或引用, 没有所有权ownership, 生命周期lifetime, 可修改性mutability和一些其他问题的困扰, 也不需要具体的约定; 它还可以防止客户和实现之间非局部的non-local交互, 这样更容易理解和维护; 这样的对象可以用在通用的API里面, 比如containers这样需要按值传递的类型;
相比一些Clone(), CopyFrom(), Swap(), copy/move ctor/assignment operator通常更容易定义, 因为它们可以由编译器产生, 隐式地或者通过 = 默认操作; 它们是简易的, 而且保证所有的数据成员被拷贝了; copy和move ctor通常也更高效, 它们不需要分配堆heap allocation或分开的初始化和赋值的步骤, 它们还适合作为优化, 比如 copy elision; (http://en.cppreference.com/w/cpp/language/copy_elision
) [复制省略 http://en.wikipedia.org/wiki/Copy_elision]
move操作允许对右值对象rvalue进行隐式而且高效地资源转移; 它允许你在某些情况下使用更简单plainer的编码风格;
缺点:
许多类型并不需要是copyable的, 为它们提供拷贝构造会令人困惑, 无意义nonsensical, 甚至彻底错误的; copy/assignment操作符对于基本类型来说是多余的hazardous, 因为使用它们会导致对象切割(http://en.wikipedia.org/wiki/Object_slicing)
默认的或无意地实现的拷贝操作符是错误的, 因此引起的bug可能是令人困惑和难以判断的;
copy ctor会隐式地被调用, 容易被忽视; 这也会造成困惑, 特别对于程序员来说, 如果他熟悉的是约定或强制使用pass-by-reference的语言, 就容易发生这种情况; 它也会带来过度拷贝Excessive copying, 造成性能问题;
结论:
如果copyable/movable对于你的类型有用的话, 就创建它们, 在其他的API中使用是会起到作用; 基本规则, 如果copy行为(包括计算复杂度computational complexity)对你的类型来说作用不明显, 就不应该是copyable的; 如果你让类型变成copyable, 要定义两个copy operations(ctor和assignment); 如果你的类型是copyable而且一个move操作比copy更有效率,
就定义两个move operations(ctor和assignment);
如果你的类型不是copyable的, 但是move对于这个类型的用户来说是正确的操作, 而且这个类型的域支持, 可以为类型定义两个move operations, 将它变为move-only的;
建议默认用 = 来定义copy和move; 定义非默认的move操作需要一个风格特例style exception; 记住要评审一下默认操作符的正确性;
由于有slicing的风险, 如果仅仅是为了继承(防止从这样的类中继承这样的成员), 不要为类提供一个assignment operator或public的 copy/move ctor; 如果基类需要是copyable, 可以提供一个virtual Clone()方法, 一个protected copy ctor, 这样可以让子类来实现它;
委托和继承构造 Delegating and Inheriting Constructors
使用委托和继承ctor来减少代码重复;
[c++11 delegating http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=296 inheriting http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=445
]
定义:
委托和继承ctor是两个不同的特性, 都是从c++11引入的, 可以有效减少ctor中的代码重复;
委托ctor允许类的一个ctor把工作转发给了一个ctor, 使用初始化列表语法中的特殊变量; e.g.
1 2 3 4 5 |
|
继承ctor允许一个子类直接使用基类的ctor, 就像是基类的其他成员函数一样, 但是必须要重新声明基类ctor; 对于基类有多个构造的时候特别有用; e.g.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
优点:
委托和继承ctor减少了冗余性verbosity和公式化boilerplate代码, 有益于可读性;
Java程序员对委托ctor很熟悉;
缺点:
委托ctor其实和使用一个helper方法是近似的approximate;
如果一个子类引入了新的成员变量, 由于基类构造并不知道它们的存在, 继承ctor可能会造成混乱;
结论:
如果可以减少公式化代码增加可读性, 就使用委托和继承ctor; 如果子类有新的成员变量时要小心使用继承ctor; 如果你能为子类成员变量使用in-class成员初始化, 继承ctor仍然是适用的appropriate;
<<<
[Remove]
3.4 拷贝构造函数
Tip 仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数; 大部分情况下都不需要, 此时应使用 DISALLOW_COPY_AND_ASSIGN; [QT中也有: Q_DISABLE_COPY]
定义: 拷贝构造函数在复制一个对象到新建对象时被调用(特别是对象传值时);
优点: 拷贝构造函数使得拷贝对象更加容易; Note STL容器要求所有内容可拷贝, 可赋值;
缺点: C++中的隐式对象拷贝是很多性能问题和bug的根源; 拷贝构造函数降低了代码可读性, 相比传引用, 跟踪传值的对象更加困难, 对象修改的地方变得难以捉摸;
结论:
大部分类并不需要可拷贝, 也不需要一个拷贝构造函数或重载赋值运算符; 不幸的是, 如果你不主动声明它们, 编译器会自动生成, 而且是public的;
Note 可以考虑在类的private中添加拷贝构造函数和赋值操作的空实现, 只有声明没有定义; 由于这些空函数声明为private, 当其他代码试图使用它们的时候, 编译器将报错; 方便起见, 可以使用 DISALLOW_COPY_AND_ASSIGN宏;
1 2 3 4 5 6 |
|
在 class Foo中:
1 2 3 4 5 6 7 8 |
|
如上所述, 绝大多数情况下都应该使用 DISALLOW_COPY_AND_ASSIGN宏; 如果类确实需要可拷贝, 应该在该类的头文件中说明原由, 并合理的定义拷贝构造函数和赋值操作; 注意在 operator=中检测自我赋值的情况; (译注: 即 operator=接收的参数是该对象本身) [利用tag, 地址, operator==等方式判别];
为了能作为STL容器的值, 你可能有使类可拷贝的冲动, 在大多数类似的情况下, 真正该做的是把对象的指针放到STL容器中; 可以考虑使用 std::tr1::shared_ptr; [智能指针代替对象]
<<<
3.5 结构体 vs 类 Structs vs. Classes
Tip 对只有数据的被动对象passive object, 使用 struct, 其他一概使用 class;
在C++中, struct和 class关键字几乎含义一样, 我们为这两个关键字添加自己的语义理解, 以便为定义的数据类型选择合适的关键字;
struct用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能; 并且存取功能是通过直接访问位域(field), 而非函数调用; 除了构造函数, 析构函数, Initialize(), Reset(), Validate()外, 不能提供其他功能的函数;
如果需要更多的函数功能, class更适合, 如果拿不准, 就用class;
为了和STL保持一致, 对于仿函数(functors)和特性(traits)可以不用class而是使用struct; [函数对象, 函数类型]
Note 类和结构体的成员变量使用不同的命名规则; [e.g. m_var和 _var]
3.6 继承 Inheritance
Tip 使用组合(composition, 译注: 这一点也是GOF在"Design Pattern"里反复强调的)常常比使用继承更合理; 如果使用继承的话, 定义为 public继承;
定义:
当子类继承基类时, 子类包含了父基类所哦有数据及操作的定义; C++实践中, 继承主要用于两种场合: 实现继承(implementation inheritance), 子类继承父类的实现代码; 接口继承(interface inheritance), 子类仅继承父类的方法名称;
优点:
实现继承通过原封不动的复用基类代码减少了代码量; 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误; 从编程的角度而言, 接口继承是用来强制类输出特定的API; 在类没有实现API中某个必须的方法时, 编译器同样会发现并报告错误;
缺点:
对于实现继承, 由于子类的实现代码散布在父类和子类之间, 要理解其实现变得更加困难; 子类不能重写父类的非虚函数, 当然也就不能修改其实现; [如果函数签名不同, 但名字相同, 就会出现重写--基类函数被隐藏] 基类也可能定义了一些数据成员, 还要区分基类的实际physical布局;
结论:
所有继承必须是public的; 如果你想使用私有继承, 应该使用成把基类的实例作为成员对象的方式;
不要过度使用实现继承; 组合常常更合适一些; 尽量做到只在"是一个"(is-a, 译注: 其他 has-a情况下请使用组合)的情况下使用继承; 如果 Bar的确"是一种" Foo, Bar才能继承Foo;
必要的话, 析构函数声明为 virtual; 如果你的类有虚函数, 则析构函数也应该为虚函数;
Note 数据成员在任何情况下都必须是私有的; 尽量不要将成员函数写成protected, 使得子类可以获得访问权;
当重载一个虚函数, 在衍生类中把它明确地声明为 virtual; 理论依据: 如果省略 virtual关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数;
[Add] 用一个override或final(不常用的)specifier显式地注释annotate 一个vitrual函数或vitrual dtor; c++11之前使用virtual关键字只是作为一个次级的注释替代; 澄清一下, 声明一个override时, 要明确使用override, final, 或virtual其中的一个; 原理Rationale:
一个函数或dtor标注为override或final, 而不是对基类virtual函数的override, 会造成编译失败, 这样可以帮助找到常见错误; specifier作用就像文档一样: 如果没有specifier, 读者不得不去检查所有的父类, 来判断该函数是否为virtual的;
[http://en.cppreference.com/w/cpp/language/final http://en.cppreference.com/w/cpp/language/override
]
<<<
3.7 多重继承 Multiple Inheritance
Tip 真正需要用到多重实现继承的情况非常少, 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类, 其它基类都是以 Interface为后缀的纯接口类;
定义: 多重继承允许子类拥有多个基类; 要作为纯接口的基类和具有实现的基类区别开来;
优点: 相比单继承, 多重实现可以复用更多的代码;
缺点: 真正需要用到多重实现继承的情况非常少, 多重实现继承看上去是不错的解决方案, 但你通常也可以找到一个更明确, 更清晰的不同解决方案;
结论: 只有当所有父类除了第一个外都是纯接口类时, 才允许使用多重继承, 为确保它们是纯接口, 这些类必须以 Interface为后缀; [有的写法是前缀 I -- Command : public ICommand]
NOTE 关于该规则, Windows下有个特例;
[菱形多重继承, 虚拟继承]
3.8 接口 Interfaces
Tip 接口是指满足特定条件的类, 这些类以 Interface为后缀(不强制);
定义:
当一个类满足以下要求时, 称之为纯接口:
- 只有纯虚函数("= 0")和静态函数(除了下文提到的析构函数);
- 没有非静态数据成员;
- 没有定义任何构造函数, 如果有也不能带有参数, 并且必须为 protected; [构造也不能是纯虚的]
- 如果它是一个子类, 也只能从满足上述条件并以 Interface为后缀的类继承;
接口不能被直接实例化, 因为它声明了纯虚函数; 为确保接口的所有实现可被正确销毁, 必须为之声明虚析构函数(作为上述第1条规则的特例, 析构函数不能是纯虚函数); 具体细节可参考 Stroustrup的 The C++ Programming Language, 3rd edition, 12.4;
[可以纯虚, 但是要有个实现 http://www.gotw.ca/gotw/031.htm 不过纯虚的dtor完全没有意义, 编译器会自动生成默认的dtor ]
优点:
以 Interface为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员, 这一点对于 多重继承尤其重要; 另外, 对于Java程序员来说, 接口的概念已经深入人心;
缺点:
Interface后缀增加了类名长度, 为阅读和理解带来了不便; 同时, 接口特性作为实现细节不应暴露给用户;
结论:
只有在满足上述需要时, 类才以 Interface结尾, 但反过来, 满足上述需要的类未必一定以 Interface结尾;
3.9 运算符重载 Operator Overloading
Tip 除少数特定情景外, 不要重载运算符;
定义: 一个类可以定义诸如 + 和 / 等运算符, 使其可以像内建类型一样直接操作; [Add]一个 operator""甚至允许内建的字面量语法去创建类对象;<<
优点:
使代码看上去更加直观, 类表现的和内建类型(如int)行为一致, 重载运算符使 Equals(), Add()等函数名黯然失色;
为了使一些模板函数正确工作, 你可能必须定义操作符;
[Add] 用户自定义的字面量可以作为明确的标记, 简便地创建用户自定义类型的对象;
缺点:
虽然操作符重载令代码更加直观, 但也有一些不足drawback:
- 混淆视听, 让你误以为一些耗时的操作和操作内建类型一样轻巧;
- 更难定位重载运算符的调用点call site, 查找 Equals()显然比对应的 == 调用点要容易得多;
- 有的运算符可以对指针进行操作, 容易导致bug; Foo + 4 做的是一件事, 而 &Foo +4 可能做的是完全不同的另一件事; 对于二者, 编译器都不会报错, 使其很难调试;
[Add]
- 用户自定义的字面量可以创建新的语法形式, 这样对C++老手来说也会觉得不习惯<<<
Note 重载还有令人吃惊的副作用ramification; 比如, 重载了 operator&的类不能安全地被前置声明;
[http://stackoverflow.com/questions/176559/you-cant-forward-declare-classes-that-overload-operator 前置声明, 然后使用 operator&,
会造成未定义的情况]
结论:
一般不要重载运算符; 尤其是赋值操作(operator=)比较诡异, 应避免重载; 如果需要的话, 可以定义类似 Equal(), CopyFrom()等函数;
[Add]同样地, 如果一个类可能被前置声明, 那么要不惜代价地防止危险的unary operator&;
不要重载 operator"", i不要引入用户自定义的字面量; [c++11 http://en.cppreference.com/w/cpp/language/user_literal ]<<<
然而, 极少数情况下可能需要重载运算符以便与模板或"标准"C++类互操作interoperate (如 operator<<(ostream&, const T&)); 只有被证明是完全合理的才能重载, 但还是要尽可能避免这样做; 尤其是不要仅仅为了在STL容器中用作键值就重载 operator==或 operator<; 相反, 你应该在声明容器的时候, 创建相等判断和大小比较的仿函数类型;
有些STL算法确实需要重载 operator==时, 你可以这么做, 记得别忘了在文档中说明原因;
参考拷贝构造函数和函数重载;
3.10 存取控制 Access Control
Tip 将所有数据成员声明为 private, 并根据需要提供相应的存取函数; (技术原因, 使用 Google Test时, 允许test fixture class的数据成员成为protected的 [http://en.wikipedia.org/wiki/Test_fixture] )
典型地, 某个名为 foo_ 的变量, 其取值函数是 foo(); 还可能需要一个赋值函数 set_foo(); [Add] 特例: static const 数据成员(一般为 kFoo)不必为private的; <<<
在头文件中定义的存取函数一般都是内联的;
参考继承和函数命名;
3.11 声明顺序 Declaration Order
Tip 在类中使用特定的声明顺序: public: 在 private: 之前, 成员函数在数据成员(变量)之前, etc.
类的访问控制区段的声明顺序依次为: public: protected: private: 如果某区段没内容, 可以不声明;
每个区段内的声明通常按以下顺序:
- typedefs 和枚举;
- 常量(static const数据成员);
- 构造函数;
- 析构函数;
- 成员函数, 含静态成员函数;
- 数据成员(除了static const数据成员)
[Add] 友元声明应该总是在private中;<< 宏 DISALLOW_COPY_AND_ASSIGN的调用(disable copy&assign)放在 private区段的末尾, 它通常是类的最后部分, 参考拷贝构造函数;
.cc文件中函数的定义应尽可能和声明顺序一致;
不要在类定义中内联大型函数; 通常, 只有那些无意义的trivial或性能要求高, 并且是比较短小的函数才能被定义为内联函数; 更多细节参考内联函数;
3.12 编写简短的函数 Write Short Functions
Tip 倾向编写简短, 凝练的函数;
我们承认长函数有时是合理的, 因此并不硬性限制函数的长度; 如果函数超过40行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割;
即使一个长函数现在工作的非常好, 一旦有人对其修改, 有可能出现新的问题, 甚至导致难以发现的bug, 使函数尽量简短, 便于他人阅读和修改代码;
在处理代码时, 你可能会发现复杂的长函数; 不要害怕intimidated修改现有代码, 如果证实这些代码使用/调试困难, 或者你需要使用其中的一小段代码, 考虑将其分割为更加简短并易于管理的若干函数;
译者笔记
1) 不在构造函数中做太多逻辑相关的初始化;
2) 编译器提供的默认构造函数不会对变量进行初始化, 如果定义了其他构造函数, 编译器不再提供, 需要编码者自行提供默认构造函数; [其实编译器会初始化, 只是初始化的动作不保证正确]
3) 为避免隐式转换, 需将单参数构造函数声明为 explicit;
4) 为避免拷贝构造函数, 赋值操作的滥用和编译器自动生成, 可将其声明为 private且无需实现;
5) 仅在作为数据集合时使用 struct;
6) 组合 > 实现继承 > 接口继承 > 私有继承, 子类重载的虚函数也要声明 virtual关键字, 虽然编译器允许不这样做;
7) 避免使用多重继承, 使用时, 除一个基类含有实现外, 其他基类均为纯接口;
8) 接口类类名以 Interface为后缀, 除提供实现的虚析构函数, 静态成员函数外, 其他均为纯虚函数, 不定义非静态数据成员, 不提供构造函数, 提供的话声明为protected;
9) 为降低复杂性, 尽量不重载操作符, 模板, 标准类中使用时提供文档说明;
10) 存取函数一般内联在头文件中;
11) 声明次序 public - protected - private;
12) 函数体尽量短小, 紧凑, 功能单一;
---TBC---YCR