类
类是c++代码的基本单元,自然使用就很广泛。本章列出了一些在写类时需要注意的事项。
构造函数的职责
不要在构造函数里做复杂的初始化。
定义:
在构造函数里做初始化操作。
优点:
方便排版,无需担心类是否已经初始化。
缺点:
1、 不容易报告错误,不能使用异常。
2、 如果操作失败,我们得到的是一个初始化是吧的对象,它有着不确定的状态。
3、 如果调用了虚函数,这些调用不会被派发到子类的实现中。将来对类的修改也好引入这个问题,即使现在类没有子类,但是还是会引发很多问题。
4、 如果有点人不小心定义了该类的全局变量,构造函数就会在main函数之前被调用,有可能打破构造函数中暗含的假定条件。例如,flag标志可能还没被初始化。
决定:
构造函数不能调用虚函数或者引发非致命的失败。如果你的对象需要一些重要的初始化,考虑用工厂函数或者init()函数。
默认构造函数
你必须定义一个默认构造函数,如果你的类定义了一个成员变量而且没有其他构造函数。否则编译器讲会生成一个默认构造函数。
定义:
当你new一个类对象而且不带参数时,默认构造函数将被调用。如果用new[]生成对象数组总是调用默认构造函数。
优点:
默认初始化结构为不可能的值,使得更容易调试。
缺点:
对于写代码的人来说,这是多余的工作。
决定:
如果你的类定义了一个成员变量而且有没有定义其他构造函数,那就必须定义一个默认构造函数。它能较好的初始化对象,使得它的状态一致有效。
这样做的理由是,如果你没有其他构造函数,也不定义默认构造函数,编译器将会为你生成一个默认构造函数。编译器生成默认构造函数可能不会那么好的初始化对象。
如果你的类继承只一个已经存在的类,而且添加了新的成员变量,你并不需要一定定义默认构造函数。
显示构造函数
用explicit关键字声明带有一个参数的构造函数。
定义:
通常,带有一个参数的构造函数可能用作类型转换。
例如,定义一个Foo::Foo(stringname),然后传递一个string给一个期待得到Foo的函数。那么这个构造函数将会被调用,把string转换为Foo,再把Foo传递给你调用的函数。这个转换可以进行,但也是麻烦的源泉,对象被转换和新对象生成超出了你的意图。用explicit
声明一个构造函数可以防止构造函数在隐式转换的情况下被调用。
优点:
避免了不良的转换
缺点:无
决定:
所有带有一个参数的构造函数都必须是显示声明的。在一个参数的构造函数定义前面加explicit
。如explicit Foo(string name);
复制构造函数例外,如果我们允许,在很少情况它们是非显示的。被其他类透明包装的类也例外。这些例外都应该写明清楚的注释。
复制构造函数
有必要的时候提复制构造函数和赋值操作。否则的话用DISALLOW_COPY_AND_ASSIGN
.进行禁止。
定义:
复制构造函数和赋值操作用于赋值对象的副本。复制构造函数有时会被编译器隐式调用,比如传值的方式传递对象。
优点:
复制构造函数能容易复制对象。STL容器需要它的内容是可以复制和能赋值的。复制构造函数比CopyFrom()函数更有效率,它结合了构造和拷贝操作,在某些环境下编译器可以省掉,很容易避免了堆内存的申请。
缺点:
隐式对象拷贝容易引发bug和性能的问题。同时也降低了代码的可读性,因为当对象被进行值传递而不是通过引用时很难进行跟踪,对象修改的地方也很难体现。
决定:
少部分的类需要可以拷贝。大部分不需要复制构造函数和赋值函数。在很多情况下,指针或者引用能跟对象的副本一样很好的工作。例如,你可以给一个函数传引用或者指针替代传值,也可以在STL容器里存指针而不是对象。
如果你的类需要可拷贝,提供一个实习复制的方法,如CopyFrom()
或 Clone()
会更好,因为这样的方法不会被隐式调用。如果一个复制的方法不足以实现功能,那就同时提供复制构造函数和赋值构造函数。
如果你的类不需要复制 函数或赋值构造函数,必须要显示禁止掉。
// A macro to disallow the copy constructorand operator= functions
// This should be used in the private:declarations for a class
#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);
};
结构体和类
当只有数据成员时用struct,其他情况都用class。
Struct和class在C++中基本上是一样的。我们为每个关键字添加了语意,所以你应该在定义数据类型时使用恰当的关键字。
Struct在只包含数据成员的消极对象使用。有时可能跟常量有关。但是不存在访问数据成员的任何函数。对其中数据的访问是直接访问而不是通过方法调用。方法不提供任何行为,仅仅是用来设置数据成员,例如构造函数,析构函数和Initialize()
, Reset()
, Validate()
.等。
如果需要更多的方法,那就用class。如果有疑问就直接用class。如果与STL结合,在仿函数和特性中可以用struct替代class。
注意:成员变量在struct和class中有不同的命名规则。
继承:
组合经常比继承更好。当使用继承时,请使用public 继承。
定义:
当子类继承自基类,它继承了父类定义的所有数据成员和操作。实际上,在C++中主要有两种继承方式:实现继承,即子类继承了父类的实现代码;接口继承,即只有方法名被继承。
优点:
实现继承通过重用基类的代码特化已存在的类型来减少了代码量。由于继承是编译时声明,你和编译器都能明白操作和发现错误。接口继承用来加强程序编程公开类特定的API。在这种情况下,如果一个类没有定义实现一个API,编译器就能发现错误。
缺点:
对于实现继承,由于实现代码在基类和子类之间实现了延伸,使得更加难以理解实现。子类不能重写非虚函数,所以子类不能改变实现代码。基类可能定义一下数据成员,因此还需指明基类物理布局。
决定:
所有的继承都应该是public继承。如果你想private继承,应该包含一个基类的实例成员。
不要过度使用实现继承。只用组合会更合适。尽量做到在“IS-A”情况下才使用继承。如果Bar可以说是一个Foo时,Bar才能是Foo的子类。
使用虚析构函数。如果你的类存在虚函数,析构函数应该是虚函数。
限定需要在子类中访问的成员函数为protect,数据成员应该为private。
当重定义一个虚函数时,应该在子类的声明里显示的声明为虚函数。原因是:如果虚函数被遗漏,读者需要检查所有的祖先类来确定一个函数是否是虚函数。
多继承
很少有多实现继承是真正有用的。当最多只有一个基类有实现,其他基类都是纯接口类时我们允许多继承
定义:
多继承允许子类有多于一个基类。我们必须区别纯接口基类和带实现的基类。
优点:
多实现继承可以让你重用比单继承更多的代码。
缺点:
只有很少的多实现继承是真正有用的。当多实现继承看起来好像是个解决方案时,其实你可以找到一个不同的更好的解决方案。
决定:
我们允许多继承,仅仅是当除了第一个类以外的所有超类都是纯接口时。为了确保他们保持纯接口,需要加上Interface
。
接口
接口是满足特定条件的类,但不是必须加Interface
后缀。
定义:
当一个类满足下列条件就是一个接口:
1、 只有public的纯虚函数和静态方法
2、 没有非静态的数据成员
3、 不需要定义任何构造函数。。如果提供构造函数,那么它必须是不带参数而且是protect的。
4、 如果是子类,它只能继承自满足这些条件的类和标记Interface
后缀的类。
接口不能被直接实例化,因为它声明了纯虚函数。为了确保所有接口的实现能被正确的销毁,接口必须声明一个虚析构函数。
优点:
用Interface标记类使其他人知道这些类不能添加实现方法或者非静态数据成员。这点在多继承时尤其重要。此外,接口这个概念已经被Java程序员所熟知。
缺点:
Interface后缀加长了类名,从而使之难读和理解。接口特性作为实现细节不应该暴露给客户。
决定:
当满足以上条件时一个类才是一个接口。但是满足以上条件的类不需要一定要Interface后缀。
操作符重载
不要重载操作符,除非特殊情况。
定义:
类可以定义操作符如+和/操作,就像是内置类型一样。
优点:
可以使代码更直观,类可以有像内置类型一样的行为。重载操作符函数有着有趣的名字,相比之下,Equals()
或 Add()
函数就逊色多了。为了某些模版很好的工作,你可能需要定义操作符。
缺点:
重载操作符使代码直观的同时也存在一些不足:
1、 它可能使直观的你误认为昂贵的操作就像内置类型那样容易实现。
2、 重载操作符很难找到它的调用处。找Equals()的调用比找==的相关调用容易得多。
3、 一些操作符对指针也起作用,从而容易引起bug。&Foo + 4
与Foo + 4
做完全不同的操作。编译器不会报错,所以也很难调试。
重载还要惊人的影响,例如,一个类重载了一元操作符&,它就很难被安全的前向声明。
决定:
通常不要重载操作符。赋值操作尤其危险,应该避免。如果需要你可以定义像Equals()
和 CopyFrom()
的方法。同样,如果一个类有可能被前向声明,物流如何都要避免重载操作符
&。
很少有为了与模版或标准C++类交互而需要重载操作符的情况。如果合情合理这些都是可接受的,如果可能你应该避免这样做。不要为了在STL容器中做key使用就重载操作符==或<。取而代之,你应该写比较和等值比较函数。
有的STL算法并不需要你重载==,如果你那么做最好提供文档说明。
访问控制
把数据成员声明为private,如果需要则提供存取函数对它们进行访问。例如你定义变量Foo_和取值函数foo(),那应该定义赋值函数set_foo().
例外:静态常量数据成员不需要是private。
取值函数的定义一般是在头文件的内联函数。
声明顺序
在类里的用特定的声明次序,public在private前面,方法在数据成员前面。
类定义开始应该是public:节,接着是protected:然后才是private。如果其中是空的节则省略掉。
在每节中声明的顺序如下:
1、 Typedefs 和 Enums
2、 Const常量
3、 构造函数
4、 析构函数
5、 方法,包括静态方法
6、 数据成员,除了静态常量数据成员
友元声明应该放在private,DISALLOW_COPY_AND_ASSIGN
宏应该放在private的末尾。
在cpp文件中的方法定义应该进可能的跟声明的顺序一样。
编写短小的函数
写小而集中的函数。
长的函数有时候也是可以的,并没有硬性规定函数的长度。如果一个函数超过40行,在不影响程序结构的情况下考虑是否可以对其进行分解。
即使现在你的长函数运行得很好,将来别人对其进行修改可能添加新的功能。由此可能引发很难发现的bug。保持你的函数小而简单,这样其他人可以更容易读懂和修改。
你会发现有的长而复杂的函数使用某些代码。不要害怕修改这些已经存在的代码,如果证明使用这样的函数很困难,很难调试或你需要在不同的地方使用其中相同的代码段,可以考虑分解函数为小而易管理的代码。