Effective C++读书笔记(12) (一)

2014-11-24 12:20:05 · 作者: · 浏览: 2

条款19:设计class 犹如设计type

Treat class design as type design

C++ 中,就像其它面向对象编程语言,可以通过定义一个新的类来定义一个新的类型。作为一个C++开发者,你的大量时间就这样花费在扩张你的类型系统。这意味着你不仅仅是一个类的设计者,而且是一个类型的设计者。重载函数和运算符,控制内存分配和回收,定义对象的初始化和终结过程——这些全在你的掌控之中。因此你应该在类设计中倾注大量心血,就如语言设计者在语言内置类型设计中所倾注的大量心血。

设计良好的类是有挑战性的,因为设计良好的类型是有挑战性的。良好的类型拥有简单自然的语法,符合直觉的语义,以及一个或更多高效的实现。那么,如何才能设计高效的类呢?首先,你必须理解你所面对的问题。实际上每一个类都需要你面对下面这些问题,其答案通常就导向你的设计规范:

· 新类型的对象应该如何创建和销毁?如何做这些将影响到你的类的构造函数和析构函数,以及内存分配和回收函数(operator new,operator new[],operator delete,和 operator delete[])的设计,除非你不写它们。

· 对象的初始化和对象的赋值应该有什么不同?这个问题的答案决定了你的构造函数和赋值运算符的行为以及它们之间的不同。

· 值传递(passed by value)对于新类型的对象意味着什么?拷贝构造函数定义了一个新类型的传值如何实现。

· 新类型的合法值是什么?通常,对于一个类的数据成员来说,仅有某些值的组合是合法的。那些数值集决定了你的类必须维护的约束条件。也决定了必须在成员函数内部进行的错误检查,特别是构造函数,赋值运算符,以及"setter"函数。它可能也会影响函数抛出的异常,以及(极少被使用的)函数异常明细(exceptionspecification)。

· 你的新类型需要配合某个继承图系中?如果你从已经存在的类继承,你就受到那些类的设计约束,特别受到它们的函数是virtual还是non-virtual的影响。如果你希望允许其他类继承你的类,将影响到你是否将函数声明为virtual,特别是你的析构函数。

· 你的新类型允许哪种类型转换?你的类型身处其它类型的海洋中,所以是否要在你的类型和其它类型之间有一些转换?如果你希望允许 T1 类型的对象隐式转型为 T2 类型的对象,你就要么在T1类中写一个类型转换函数(如operator T2),要么在 T2 类中写一个non-explicit-one argument构造函数。如果你只允许显示构造函数存在,就得写出专门负责执行转换的函数,且不得为类型转换操作符或non-explicit-oneargument构造函数。

· 对于新类型哪些运算符和函数是合理的?这个问题的答案决定你为你的类声明哪些函数。其中一些是成员函数,另一些不是。

· 哪些标准函数应该驳回?你需要将那些都声明为 private。

· 你的新类型中哪些成员可以被访问?这个问题的可以帮助你决定哪些成员是 public,哪些是 protected,以及哪些是 private。它也可以帮助你决定哪些类 和/或 函数应该是友元,以及一个类嵌套在另一个类内部是否有意义。

· 什么是新类型的未声明接口 "undeclaredinterface"?它对于效率,异常安全,以及资源使用(例如,多任务锁定和动态内存)提供哪种保证?你在这些领域提供的保证将为你的类的实现代码加上相应的约束条件。

· 你的新类型有多大程度的通用性?也许你并非真的要定义一个新的类型,也许你要定义一整个类型家族。如果是这样,你就不该定义一个新的类,而应该定义一个新的类模板。

· 一个新的类型真的是你所需要的吗?是否你可以仅仅定义一个新的继承类,以便让你可以为一个已有的类增加一些功能,也许通过简单地定义一个或更多非成员函数或模板能更好地达成你的目标。

· 类设计就是类型设计。定义高效的类是有挑战性的。在C++中用户自定义类生成的类型最好可以和内建类型一样好。

条款20:宁以pass-by-reference-to-const替换pass-by-value

Prefer pass-by-reference-to-const to pass-by-value

缺省情况下,C++以传值方式将对象传入或传出函数(这是一个从C继承来的特性)。除非你另外指定,否则函数的参数就会以实际参数的副本进行初始化,而函数的调用者会收到函数返回值的一个复件。这个复件由对象的拷贝构造函数生成,这就使得传值成为一个代价不菲的操作。例如,考虑下面这个类继承体系:

class Person {
public:
Person(); // 为求简化,省略参数
virtual ~Person();
...

private:
std::string name;
std::string address;
};

class Student: public Person {
public:
Student(); // 再次省略参数
~Student();
...

private:
std::string schoolName;
std::string schoolAddress;
};

现在,考虑以下代码,在此我们调用函数validateStudent,它得到一个Student实参(以传值方式),并返回它是否有效:

bool validateStudent(Student s); // 函数以by value方式接受Student

Student plato;

bool platoIsOK = validateStudent(plato); //call the function

很明显,Student的拷贝构造函数被调用,用plato来初始化参数s。同样明显的是,当 validateStudent返回时,s就会被销毁。所以这个函数的参数传递代价是一次Student的拷贝构造函数的调用和一次Student的析构函数的调用。

但这还不是全部。Student对象内部包含两个string对象,Student对象还要从一个 Person对象继承,Person对象内部又包含两个额外的string对象。最终,以传值方式传递一个Student对象的后果就是引起一次Student的拷贝构造函数的调用,一次Person的拷贝构造函数的调用,以及四次string的拷贝构造函数调用。当Student对象的拷贝被销毁时,每一个构造函数的调用都对应一个析构函数的调用,所以以传值方式传递一个Student的全部代价是六个构造函数和六个析构函数!

这是正确和值得的行为。毕竟,你希望全部对象都得到可靠的初始化和销毁。尽管如此,pass by reference-to-const方式会更好:

bool validateStudent(const Student& s);

这样做非常有效:没有任何构造函数和析构函数被调用,因为没有新的对象被构造。修改后参数声明中的const是非常重要的,原先validateStudent以by-value方式接受一个Student参数,所以调用者知道函数绝不会对它们传入的Student做任何改变,validateStudent只能改变它的复件。现在Student以引用