与其它的面向对象编程语言类似,在C++中,定义一个新的class即定义了一个新的类型。一个C++开发者的职业生涯的大多数时间都将用在“不断丰富充实他们的类型系统”上。这意味着他不仅仅是一个class的设计者,更是一个类型的设计者。函数和运算符重载、内存的分配和释放控制、对象初始化和终止定义——一切都由设计人员手工完成。我们知道,语言设计人员在设计内建数据类型时倾注了大量心血,而一个class设计人员也要花费同样的精力。
能否设计出优秀的class对于设计人员来说是一项严峻的考验,因为设计出好的数据类型本身就是一项艰巨的任务。优秀的类型拥有自然的语法、直观的语义,并且还有一套或多套高效的实现。在C++中,如果定义class的工作做得一团糟,那么期望达到上面的目标就是天方夜谭。甚至class的成员函数的声明方式也会影响到它的性能。
那么,如何把class设计得更高效呢?首先,你必须要了解你所面对的问题。几乎所有的class设计都将面对下面的问题,它们的答案可以对设计起到一定的约束作用:
l 新类型的对象应如何创建和删除?class中与之相关的函数包括:构造函数和析构函数,以及class中其它的内存分配和释放函数(operator new、operator new[]、operator delete、operator delete[],参见第八章)。如果你自己手动编写它们,这个问题的解决方式将会影响到这些函数。
l 对象初始化与对象赋值有怎样的不同?这个问题的答案决定着构造函数与赋值运算符之间的区别。不要混淆初始化和赋值的概念,这一点很重要,因为二者所面对的函数调用类型是不同的。
l 新类型在通过传值方式传递对象时意味着什么?请牢记,一个类型是通过拷贝构造函数来定义传值操作的实现方式的。
l 新类型对合法数值有哪些限制?通常情况下,对于某个class的数据成员而言,只有一些特定的数值组合是合法的。这些组合决定了class应遵循哪些定律。而这些定律又决定了在数据成员中你应该进行哪些错误检查,尤其是构造函数、赋值运算符、以及“设定”函数(即setter)。它们还会影响到函数会抛出什么样的异常,同时在某些情况下还有可能影响到函数所抛出异常的细节。
l 新类型是否适用于继承?如果新的class由现有的class继承而来,那么新的class应遵循现有class(即父类)设计方案的限制。尤其是要确定父类的成员函数是否为虚函数(参见条目34和36)。如果期望让其它的class可以继承当前的class,就需要考虑当前class的成员函数是否应为虚函数,尤其是它的析构函数(参见条目7)。
l 新类型允许进行哪些类型转换?新的类型存在于各式各样的类型之间,那么是否应该提供新类型与其它类型的类型转换功能呢?如果你期望让T1的一个对象将类型隐式转换为T2。可以通过在T1类中放置一个类型转换函数(比如operator T2),或者在T2类中放置一个有单一参数的非explicit构造函数。如果你期望T1仅允许显式类型转换,就需要编写函数来执行这一转换,但是这一函数不应是类型转换运算符,也不应是单一参数的非explicit构造函数。(条目15中有隐式/显式转换函数的示例。)
l 哪些运算符和函数对新类型是有意义的?这个问题的答案取决于你会为你的class声明哪些函数。一些函数将成为成员函数,另一些则不是(参见条目23、24、46)。
l 应明确拒绝哪些标准函数?通过将它们声明为private的可达到这一目的(参见条目6)。
l 谁可以访问新类型中的数据成员?这一问题可以帮助我们确定哪些成员应为public的,哪些是protected的,以及哪些是private的。同时,也可以帮助我们确定哪些class和/或函数应该是友元,还有嵌套的class是否有意义。
l 新类型中有哪些“未声明的接口”?如果你充分考虑了新类型中性能、异常安全(参见条目29)、资源使用(比如互斥锁、动态内存)等问题,系统将许诺给你什么呢?我们说你在这些领域所作出的努力,将确保你的class的实现中相应的约束条件能够得以严格实施。
l 新类型有多通用?可能你想做的并不仅仅是定义一个新类型。而是定义一族新类型。如果真是这样,需要你定义的就不是一个新的class了,你需要定义一个新的类模板(class template)。
l 你真的需要一个新类型吗?如果你创建新的派生类仅仅为了为现有的类添加新的功能,那么通过简单地定义一个或多个非成员函数或者模板可能会更好的达到目标。
完整地回答以上的问题列表并不是一件简单的事情,因此定义高效的class就是一项严峻的挑战。然而,如果成功完成了这一挑战,那么由用户自定义的class生成的类型至少可以像内建数据类型一样好用。一切都是值得的。
时刻牢记
l class设计就是类型的设计。在定义一个新的类型之前,要确保将本条目讨论的所有问题考虑周全。