1.构造函数的职责
构造函数只进行那些没有实际意义的初始化,可能的话,使用Init()方法集中初始化为有意义的数据
定义:在构造函数中执行初始化操作
优点:排版方便,无需担心类是否初始化
缺点:
1)构造函数中不易报告错误,不能使用异常
2)操作失败会造成对象初始化失败,引起不确定状态
3)构造函数内调用虚函数,调用不会派发到子类实现中,即使当前没有子类化实现,将来仍是隐患
4)如果有人创建该类型的全局变量,构造函数酱紫啊main()之前被调用,有可能破坏构造函数中暗含的假设条件,一些条件未初始化等
结论:如果对象需要有意义的初始化,考虑使用另外的Init()方法并增加一个成员标记用于指示对象是否已经初始化成功
2.默认构造函数
如果一个类定义了若干成员变量又没有其他构造函数,需要定义一个默认构造函数,否则编译器将自动生产默认构造函数
定义:新建一个没有参数的对象时,默认构造函数被调用,当调用new时,默认构造函数总是被调用
优点:默认将结构体初始化为“不可能的”值,使调试更加容易
缺点:对代码编写者来说,这是多余的工作
结论:如果类中定义了成员变量,没有提供其他构造函数,你需要定义一个默认构造函数,默认构造函数更适合于初始化对象,使对象内部状态一致、有效。
如果你定义地类继承现有类,而你又没有增加新的成员变量,则不需要为新类定义默认构造函数
3.明确的构造函数
对单参数构造函数使用C++关键字explicit
定义:通常,是有一个参数的构造函数可被用于转化(主要指隐式转化),例如,定义了Foo::Foo(string name),当向需要传入一个Foo对象的函数传入一个字符串时,构造函数Foo::Foo(string name) 被调用并将该字符串转换为一个Foo临时对象传给调用函数。看上去很方便,但如果你不希望如此通过转化生成一个新对象的话,麻烦也随之而来。为避免构造函数被调用造成隐式转化,可以将其声明为explicit。
优点:避免不合时宜的变换
缺点:无
结论:所有单参数构造函数必须是明确的。在类定义中,将关键字explicit加到单参数构造函数前:explicit Foo(string name);
例外:在少数情况下,拷贝构造函数可以不声明为explicit;特意作为其他类的透明包装器的类。类似例外的情况应该在注释中明确说明。
4.拷贝构造函数
仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数;不要要靠背时应使用DISALLOW_COPY_AND_ASSIGN。
定义:通过拷贝新建对象时可使用拷贝构造函数(特别是对象的传值时)
优点:拷贝构造函数使得拷贝对象更加容易, STL容器要求所有内容可靠被。可赋值
缺点:C++中对象的隐式拷贝是导致很多性能问题和bugs的根源。拷贝构造函数降低了代码可读性,相比按引用传递,跟踪按值传递的对象更加困难,对象修改的地方变得难以捉摸
结论:大量的类并不需要可拷贝,也不需要一个拷贝构造函数或赋值操作。不幸的是,如果你不主动声明它们,编译器就会为你自动生成,而且是public的。
可以考虑在类的private中添加空的拷贝构造函数和赋值操作,只有声明,没有定义。由于这些空程序声明为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);
}
如果类确实需要可拷贝,应该在类的头文件中说明原由,并适当定义拷贝构造函数和赋值操作,注意在operator=中检测自赋值情况
将类作为STL容器值的时候,你可能有使类可拷贝的冲动,类似情况下,真正应该做的是使用指针指向STL容器中的对象,可以考虑使用std::tr1::shared_ptr。
5.结构体和类
仅当只有数据时使用struct,其他一概用class
struct被用在仅包含数据的消极对象上,可能包括有关联的常量,但没有存取数据成员之外的函数功能,而存取功能通过直接访问实现无需方法调用,这儿提到的方法是指只用于处理数据成员的,如构造函数,析构函数、Initialize()、Reset(),Validate()。
如果与STL结合,对于仿函数(funtors)和特性(traits)可以不用class而是使用struct
所谓的仿函数就是定义了operator()的对象,就是使一个类看上去想一个函数。如:
class Test{
public:
Test operator(//可以设置参数){
}
}
注意:类和结构体的成员变量使用不同的命名规则
6.继承
使用组合通常比使用继承更适宜,如果使用继承的话,只使用公共继承。
定义:当子类继承基类时,子类包含父基类所有数据及操作的定义。C++实践中,继承主要用于两种场合:实现继承(子类继承父类的实现代码);接口继承(子类继承父类的方法名称)
优点:
实现继承通过原封不动的重用基类代码减少了代码量,由于继承是编译时声明,编码者和编译器都可以理解相应操作并发现错误。
接口继承可用于程序上增强类的特定API的功能,在类没有定义API的必要实现时,编译器同样可以侦错。
缺点:对于实现继承,由于子类的代码在父类和子类间延展,要理解其实变得更加困难、子类不能重写父类的虚函数,当然也不能修改其实现。基类也可能定义了一些数据成员,还要区分基类的屋物理轮廓
结论:
所有继承必须是public的,如果想私有继承的话,应该采取包含基类实例作为成员的方式作为替代。
不要过多使用实现继承,组合通常更适合一些。努力做到只在“是一个”("is-a",译者注,其他“has-a”情况下请使用组合)的情况下使用继承:如果Bar的确“是一种”Foo,才令Bar是Foo的子类
必要的话命令析构函数为virtual,必要是指,如果该类具有虚函数,其析构函数应该是为虚函数。
注:至于子类没有额外的数据成员,甚至父类也没有任何数据成员的特殊情况下,析构函数的调用是否必要是语义争议。从编程设计规范的角度看,在含有虚函数的父类中,定义虚析构函数绝对必要。
限定仅在子类中访问的成员函数为protected,需要注意的是数据成员应该始终为私有。
当重定义派生的虚函数时,在派生类中明确声明其为virtual。根本原因:如果遗漏virtual,阅读者需要检索类的所有祖先已确定该函数是否为虚函数(注,虽然不影响其为虚函数的本质)
对于虚函数强调几个概念:
1)定义一个函数为虚函数,不代表函数为不被实现的函数。
2)定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
3)定义一个函数为纯虚函数,才代表函数没有被实现。
4)定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
- 多重继承
只有当最多一个基类中含有实现,其他基类都是以Interface为后缀的纯接口类时才会使用多重继承
定义:多重继承允许子类拥有多个基类,要将作为纯接口的基类和具有实现的基类区别开来。
优点:相比单继承,多重实现继承可令你重用更多代码
缺点:真正用到多重继承很少,多重实现继承看上去不错,但通常可以找到更加明确、清晰的、不同的解决方案
结论:只有当所有超类出第一个外都是纯接口时才能使用多重继承。为确保他们是纯接口,这些类必须以Interface为后缀。
注意:关于此规则,Windows下有种例外情况
- 接口
接口是指满足特定条件的类,这些类以Interface为后缀(非必须)。
定义:当一个类满足以下要求时,被称为纯接口:
1)只有纯虚函数("=0")和静态函数(下文提到的析构函数除外)
2)没有非静态数据成员
3)没有定义任何构造函数。如果有,也不含参数,并且为protected
4)如果是子类,也只能继承满足上述条件并以Interface为后缀的类
接口类不能直接被实例化,因为它声明了纯虚函数。为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数(作为第一条规则的例外,析构函数不能是纯虚函数)。
优点:以Interface为后缀可令他人知道不能为该接口类增加实现函数或者非静态数据成员忙着一点对于多重继承尤为重要。另外,对于java程序员来说,接口的该理念已经深入人心
缺点:Interface后缀增加了类名长度,为阅读和理解带来不便,同时,接口特性作为实现细节不应暴露给客户。
结论:只有在满足上述需求时,类才以Interface结尾,但反过来,满足上述需要的类未必一定以Interface结尾。
9.操作符重载
定义:一个类可以定义诸如+、/等操作,使其可以像内建类型一样直接使用
优点:使代码看上去更直观,像内建类型(如int)那样,重载操作符使那些Equals()、Add()等黯淡无光的函数名无用。为了使一些模板函数正确工作,你可能需要定义操作符。
缺点:虽然操作符重载令代码更加直观,但也有一些不足:
1)混淆直觉,让你误以为一些好事的操作像内建操作那样轻巧
2)查找重载操作符的调用处更加困难,查找Equals()显然比同等调用==容易得多
3)有的操作符可以对指针进行操作,容易导致bugs,Foo + 4 做的是一件事,而&Foo + 4 可能做的是完全不同的另一件事,对于二者,编译器都不会报错,使其很难调试。
结论:
一般不要重载操作符,尤其赋值操作符(operator==)比较阴险,应避免重载。如果需要的话,可以定义类似Equals()、CopyFrom()等函数。
然而及少数情况下需要重载操作符易边与模板或者“标准C++类衔接”(如operator<<(ostream&, const T&)),如果被证明是正当的尚可接受,但你要尽可能避免这样做。尤其是不要仅仅为了在STL容易中作为key使用就重载operator==或者operator<,取而代之,你应该在晟敏容器的时候,创建相等判断和大小比较的仿函数类型。
有些STL算法确实需要重载operator==时可以这么做,但不要忘了提供文档说明
参考拷贝构造函数和函数重载
10.存取控制
将数据成员私有化,并提供相关存取函数,如定义foo_及取值函数foo()、赋值函数set_foo().
存取函数的定义一般内联在头文件中。
参考继承和函数命名。
11.声明次序
在类的使用特定的声明次序:public:在private:之前,成员函数在数据成员(变量)前。
定义次序如下:public:、protected:、private:,如果那一块没有,忽略即可
每一块中,声明次序如下:
1)typedefs 和enums
2)常量
3)构造函数
4)析构函数
5)成员函数,含静态成员函数
6)数据成员,含静态数据成员
宏DISALLOW_COPY_AND_ASSIGN置于private:块之后,作为类的最后部分。参考构造函数。
c文件中函数的定义尽可能和声明的次序一致
不要将大型的函数内联到类的定义中,通常。至于那些没有特别意义或者性能要求高的,并且比较短小的函数才被定义为内联函数。
- 编写短小函数
长函数有时是恰当的,如果函数超过40行,可以考虑在不影响程序结构的情况下将其分割一下。
函数尽量短小、简单,便于他人阅读和修改代码
13.总结:
1)不在构造函数中做太多逻辑相关的初始化
2)编译器提供的默认构造函数不会对变量进行初始化,如果定义了其他构造函数,编译器不再提供,需要编码这自行提供默认构造函数
3)为避免隐式转换,需将单参数构造函数声明为explicit
4)为避免拷贝构造函数、赋值操作的滥用和编译器自动生成,可目前声明其为private且无需实现
5)仅在作为数据集合时使用struct
6)组合 > 实现继承 > 接口继承 > 私有继承,子类重载的虚函数也要声明virtual关键字,虽然编译器允许不这样做
7)为避免使用多重继承,使用时,除一个基类含有实现外,其他基类均为纯接口
8)接口类类名以Interface为后缀,除提供待实现的虚析构函数,静态成员函数外,其他均为纯虚函数,不定义非静态数据成员,不提供构造函数,提供的话,声明为protected
9)为降低复杂性,尽量不重载操作符,模板、标准类中使用时提供说明文档
10)存取函数一般内联在头文件中
11)声明次序:public、protected、private
12)函数体应尽量短小。紧凑。功能单一。