当前位置:编程学习 > C/C++ >>

Google C++编程风格指南(三):C++类

关于类的注意事项,总结一下:1. 不在构造函数中做太多逻辑相关的初始化; 2. 编译器提供的默认构造函数不会对变量进行初始化,如果定义了其他构造函数,编译器不再提供,需要编码者自行提供默认构造函数;3. 为避免隐式转换,需将单参数构造函数声明为explicit;……

类是C++中基本的代码单元,自然被广泛使用。本节列举了在写一个类时要做什么、不要做什么。
1. 构造函数(Constructor)的职责
构造函数中只进行那些没有实际意义的(trivial,译者注:简单初始化对于程序执行没有实际的逻辑意义,因为成员变量的“有意义”的值大多不在构造函数中确定)初始化,可能的话,使用Init()方法集中初始化为有意义的(non-trivial)数据。
定义:在构造函数中执行初始化操作。
优点:排版方便,无需担心类是否初始化。
缺点:在构造函数中执行操作引起的问题有:
1) 构造函数中不易报告错误,不能使用异常。
2) 操作失败会造成对象初始化失败,引起不确定状态。
3) 构造函数内调用虚函数,调用不会派发到子类实现中,即使当前没有子类化实现,将来仍是隐患。
4) 如果有人创建该类型的全局变量(虽然违背了上节提到的规则),构造函数将在main()之前被调用,有可能破坏构造函数中暗含的假设条件。例如,gflags尚未初始化。
结论:如果对象需要有意义的(non-trivial)初始化,考虑使用另外的Init()方法并(或)增加一个成员标记用于指示对象是否已经初始化成功。
2. 默认构造函数(Default Constructors)
如果一个类定义了若干成员变量又没有其他构造函数,需要定义一个默认构造函数,否则编译器将自动生产默认构造函数。
定义:新建一个没有参数的对象时,默认构造函数被调用,当调用new[](为数组)时,默认构造函数总是被调用。
优点:默认将结构体初始化为“不可能的”值,使调试更加容易。
缺点:对代码编写者来说,这是多余的工作。
结论:
如果类中定义了成员变量,没有提供其他构造函数,你需要定义一个默认构造函数(没有参数)。默认构造函数更适合于初始化对象,使对象内部状态(internal state)一致、有效。
提供默认构造函数的原因是:如果你没有提供其他构造函数,又没有定义默认构造函数,编译器将为你自动生成一个,编译器生成的构造函数并不会对对象进行初始化。
如果你定义的类继承现有类,而你又没有增加新的成员变量,则不需要为新类定义默认构造函数。

3. 明确的构造函数(Explicit Constructors)
对单参数构造函数使用C++关键字explicit。
定义:通常,只有一个参数的构造函数可被用于转换(conversion,译者注:主要指隐式转换,下文可见),例如,定义了Foo::Foo(string name),当向需要传入一个Foo对象的函数传入一个字符串时,构造函数Foo::Foo(string name)被调用并将该字符串转换为一个Foo临时对象传给调用函数。看上去很方便,但如果你并不希望如此通过转换生成一个新对象的话,麻烦也随之而来。为避免构造函数被调用造成隐式转换,可以将其声明为explicit。
优点:避免不合时宜的变换。
缺点:无。
结论:
所有单参数构造函数必须是明确的。在类定义中,将关键字explicit加到单参数构造函数前:explicit Foo(string name);
例外:在少数情况下,拷贝构造函数可以不声明为explicit;特意作为其他类的透明包装器的类。类似例外情况应在注释中明确说明。
4. 拷贝构造函数(Copy Constructors)
仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数;不需要拷贝时应使用DISALLOW_COPY_AND_ASSIGN。
定义:通过拷贝新建对象时可使用拷贝构造函数(特别是对象的传值时)。
优点:拷贝构造函数使得拷贝对象更加容易,STL容器要求所有内容可拷贝、可赋值。
缺点:C++中对象的隐式拷贝是导致很多性能问题和bugs的根源。拷贝构造函数降低了代码可读性,相比按引用传递,跟踪按值传递的对象更加困难,对象修改的地方变得难以捉摸。
结论:
大量的类并不需要可拷贝,也不需要一个拷贝构造函数或赋值操作(assignment operator)。不幸的是,如果你不主动声明它们,编译器会为你自动生成,而且是public的。
可以考虑在类的private中添加空的(dummy)拷贝构造函数和赋值操作,只有声明,没有定义。由于这些空程序声明为private,当其他代码试图使用它们的时候,编译器将报错。为了方便,可以使用宏DISALLOW_COPY_AND_ASSIGN:
// 禁止使用拷贝构造函数和赋值操作的宏
// 应在类的private:中使用
#define DISALLOW_COPY_AND_ASSIGN(TypeName)
TypeName(const TypeName&);
void operator=(const TypeName&)

class Foo {
public:
Foo(int f);
~Foo();

private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};

如上所述,绝大多数情况下都应使用DISALLOW_COPY_AND_ASSIGN,如果类确实需要可拷贝,应在该类的头文件中说明原由,并适当定义拷贝构造函数和赋值操作,注意在operator=中检测自赋值(self-assignment)情况。
在将类作为STL容器值得时候,你可能有使类可拷贝的冲动。类似情况下,真正该做的是使用指针指向STL容器中的对象,可以考虑使用std::tr1::shared_ptr。

5. 结构体和类(Structs vs. Classes)
仅当只有数据时使用struct,其它一概使用class。
在C++中,关键字struct和class几乎含义等同,我们为其人为添加语义,以便为定义的数据类型合理选择使用哪个关键字。
struct被用在仅包含数据的消极对象(passive objects)上,可能包括有关联的常量,但没有存取数据成员之外的函数功能,而存取功能通过直接访问实现而无需方法调用,这儿提到的方法是指只用于处理数据成员的,如构造函数、析构函数、Initialize()、Reset()、Validate()。
如果需要更多的函数功能,class更适合,如果不确定的话,直接使用class。
如果与STL结合,对于仿函数(functors)和特性(traits)可以不用class而是使用struct。
注意:类和结构体的成员变量使用不同的命名规则。
6. 继承(Inheritance)
使用组合(composition,译者注,这一点也是GoF在《Design Patterns》里反复强调的)通常比使用继承更适宜,如果使用继承的话,只使用公共继承。
定义:当子类继承基类时,子类包含了父基类所有数据及操作的定义。C++实践中,继承主要用于两种场合:实现继承(implementation inheritance),子类继承父类的实现代码;接口继承(inte易做图ce inheritance),子类仅继承父类的方法名称。
优点:实现继承通过原封不动的重用基类代码减少了代码量。由于继承是编译时声明(compile-time declaration),编码者和编译器都可以理解相应操作并发现错误。接口继承可用于程序上增强类的特定API的功能,在类没有定义API的必要实现时,编译器同样可以侦错。
缺点:对于实现继承,由于实现子类的代码在父类和子类间延展,要理解其实现变得更加困难。子类不能重写父类的非虚函数,当然也就不能修改其实现。基类也可能定义了一些数据成员,还要区分基类的物理轮廓(physical layout)。
结论:
所有继承必须是public的,如果想私有继承的话,应该采取包含基类实例作为成员的方式作为替代。
不要过多使用实现继承,组合通常更合适一些。努力做到只在“是一个”("is-a",译者注,其他"has-a"情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo,才令Bar是Foo的子类。
必要的话,令析构函数为virtual,必要是指,如果该类具有虚函数,其析构函数应该为虚函数。
译者注:至于子类没有额外数据成员,甚至父类也没有任何数据成员的特殊情况下,析构函数的调用是否必要是语义争论,从编程设计规范的角度看,在含有虚函数的父类中,定义虚析构函数绝对必要。
限定仅在子类访问的成员函数为protected,需要注意的是数据成员应始终为私有。
当重定义派生的虚函数时,在派生类中明确声明其为virtual。根本原因:如果遗漏virtual,阅读者需要检索类的所有祖先以确定该函数是否为虚函数(译者注,虽然不影响其为虚函数的本质)。
7. 多重继承(Multiple Inheritance)
真正需要用到多重实现继承(multiple implementation inheritance)的时候非常少,只有当最多一个基类中含有实现,其他基类都是以Inte易做图ce为后缀的纯接口类时才会使用多重继承。
定义:多重继承允许子类拥有多个基类,要将作为纯接口的基类和具有实现的基类区别开来。
优点:相比单继承,多重实现继承可令你重用更多代码。
缺点:真正需要用到多重实现继承的时候非常少,多重实现继承看上去是不错的解决方案,通常可以找到更加明确、清晰的、不同的解决方案。
结论:只有当所有超类(superclass)除第一个外都是纯接口时才能使用多重继承。为确保它们是纯接口,这些类必须以Inte易做图ce为后缀。
注意:关于此规则,Windows下有种例外情况(译者注,将在本译文最后一篇的规则例外中阐述)。

8. 接口(Inte易做图ce)
接口是指满足特定条件的类,这些类以Inte易做图ce为后缀(非必需)。
定义:当一个类满足以下要求时,称之为纯接口:
1) 只有纯虚函数("=0")和静态函数(下文提到的析构函数除外);
2) 没有非静态数据成员;
3) 没有定义任何构造函数。如果有,也不含参数,并且为protected;
4) 如果是子类,也只能继承满足上述条件并以Inte易做图ce为后缀的类。
接口类不能被直接实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(作为第1条规则的例外,析构函数不能是纯虚函数)。具体细节可参考Stroustrup的《The C++ Programming Language, 3rd edition》第12.4节。
优点:以Inte易做图ce为后缀可令他人知道不能为该接口类增加实现函数或非静态数据成员,这一点对于多重继承尤其重要。另外,对于Ja

补充:软件开发 , C++ ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,