Effective C++ ——设计与声明
条款18:让接口更容易的被使用,不易误用
接口设计主要是给应用接口的人使用的,他们可能不是接口的设计者,这样作为接口的设计者就要对接口的定义更加易懂,让使用者不宜发生误用,例如对于一个时间类:
class Date{ public: Data(int month, int day, int year){ .... } };在应用Date类的时候,对于Date的三个参数我们很容易用错,因为它们的类型相同,我们可能会将实参20传给month等,我们在设定接口的时候要保证接口使用过程中不管怎么用都会不出错,例如我们可以定义三个类型:
struct month{ explicit month(int i):val(i){} int val; }; struce day{ explicit day(int i):val(i){} int val; }; struct year{ explicit year(int i):val(i){} int val; };这样我们在使用的时候就不会出现将day参数和month参数用错的情况,例如:
Date d(month(12),day(19),year(2013));
但是此时我们可能经month的参数用错,例如设置为>12,或者将day的参数大于该月的天数,这样我们可以经一步设置:
class month{ public: static Month Jan(){return Month(1);} static Month Feb(){return Month(2);} ..... static Month Dec(){return Month(12);} private: explicit month(int m):val(m){} int val; };这样我们就可以调用:
Date d(month::Jan(),day(18),year(2013));我们用类的静态函数来替换对象来制定月份,相关的我们也可以设定day和year,这样我们就不会出现Month用错的情况了。
我们设定接口的一个原则就是限定类型内什么事情可以做,什么事情不可以做,我们在设定一个类型的时候,我们要让它与内置的类型有相同的表现形式。
当我们的接口返回的是指针的类型的时候,应用接口的客户可能会出现对指针所指向资源没有释放的情况,我们此时可以使用前面介绍过的资源管理类来对资源进行管理,常用的是std::tr1::shared_ptr智能指针,它可以自己制定其所指资源释放的时候所调用的函数,具体使用方式请参看相关资料。
请记住:
- 好的接口要容易被正确使用而不是误用
- "促进正确使用"的方法包括接口的一致性,与内置类型的行为兼容等
- "阻止误用"的方法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户对资源的管理责任。
- tr1::shared_ptr支持定制型删除器;
条款19:设计class犹如设计type
条款主要的是介绍在设计class的时候应该注意的问题,只要有以下几点:
- 新的type的对象应该如何被创建和销毁?主要是指的新的type的构造函数和析构函数的定义。
- 对象的初始化和对象的赋值有什么样的区别?主要是新type的构造函数和赋值函数的定义。
- 新的type的对象如果被passed by value(以值传递),意味着什么? 记住拷贝构造函数用来定义一个type的passed by value该如何实现。
- 什么是新的type的“合法值”?在class类中包含各种的成员变量,这些成员变量的取值范围是什么?
- 你的新的type需要配合某个继承图系吗?如果你继承自某些既有的class,你就受到那些class的设计的束缚,特别是受到“它们的函数是virtual或non-virtual”的影响。
- 你的新type需要什么类型的转化?主要是指的隐式转化和显示转化。
- 什么样的操作符和函数对该新type来说是合理的?主要是指的该type需要定义的member函数。
- 什么样的标准函数应该被驳回?那些正是你必须声明为private者。
- 谁该取用新的type的成员?主要是新type的public/private/protected函数或者friend函数。
- 什么是新的type的“未声明接口”?
- 你的type有多么的一般化?如果他是一个type的家族,那么可以试着写下class template。
- 是否真的需要一个type类型?如果定义dervie class 只是为base class添加新的技能,那么可以优先考虑non-member和friend函数。
请记住:
- class设计即使type的设计,在定义一个新的type的时候,请确定你已经考虑过本条款所讨论的主题。
class People{ public: Person(); virtual ~People(); ... private: std::string name; std::string address; }; class Student:public People{ public: Studenet(); ~Student(); ... private: std::string schoolName; std::string shoolAddress; };
考虑下面的调用:
bool validateStudent(Student s); Student plato; bool platoIsOk = validateStudent(plato);在函数validateStudent函数调用的时候会发生什么事情?首先会调用Student的copy构造函数从plato copy构造对象s,在函数validateStudent函数执行结束后,调用对象s的析构函数,这样总共调用了一次copy 构造函数和一次析构函数,但是在Student类内部,有两个string成员对象,因此在Student拷贝构造函数的时候也会调用string类的两个拷贝构造函数,相应结束的时候也会调用string的析构函数,又因为Student类继承子People,因此Student类的构造会引起People类对象的构造,对应的People对象两个string对象的拷贝构造和对应的析构函数,这样一个函数的执行就涉及到了六次的拷贝构造函数(一次Student,一次People,四次string)和六次的析构函数,虽然调用时正确的,但是调用的效率是太低了,我们可以通过pass-by-reference-to-const来调用。
bool validateStudent(const Student& s);通过引用的调用可以防止对应的构造函数与析构函数的调用,const关键字主要是用来说明实参的对象是不能改变的,在pass-by-value的调用方式中,实参的对象也是不能改变的,改变的是复制的临时对象!在函数返回的对象也是有相似的情况!
在pass-by-value还可能造成一种对象切割的情况,例如:
class Window{ public: ... std::string name() const; virtual void display() const; }; class WindowWithScrollBars:public Window{ public: ... virtual void display() const; .... }; void printNameAndDisplay(Window w){ std::cout << w.name()<<endl; w.display(); } WindowWithScrollBars wwsb; printNameAndDisplay(wwsb);在上面的这种情况下,当wwsb传递给函数的参数w时,发生的是pass-by-value,此时调用的Window的copy构造函数,因此,copy 构造函数构造的对象是Window类型的,不管实参类型是Window类型还是Window的子类型,这种情况叫对象的切割,对应的如果我们将printNameAndDisply函数的参数修改为const Window& w,此时我们就可以在函数内部调用到子类的函数,这就是多态的应用!需要注意!
在C++中并不是所有的对象都是要以pass-by-reference-to-const的形式,对于内置的类型和stl中的迭代器类型和函数对象还是要采用pass-by-value的形式!
请注意:
- 尽量以pass-by-reference-to-const来替换pass-by-value,前者不仅高效还能防止对象的切割问题。
- 对于内置类型和STL中的迭代器和函数对象还是要采用pass-by-value的形式比较高效。
- 如果你有个对象属于内置类型(例如 int),pass by value 往往比pass by reference的效率高些。
class Ration{ public: Ration(int numerator = 0,int denominator = 1); ... private: int n,b; friend const Ration operator*(const Ration& lhs,const Ration& rhs); }; const Ration operator*(const Ration& lhs, const Ration& rhs){ Ration result(lhs.n * rhs.n, lhs.b * rhs.b); return result; }上面是个有理数的类,在操作符*中我们返回值一个pass-by-value的形式进行的,下面我们看下如果pass-by-reference会出现什么情况:
const Ration& operator*(const Ration& lhs,const Ration& rhs){ Ration result(lhs.n * rhs.n, lhs.b * rhs.b); return result; }上面的例子中,在函数内result的对象是在函数的堆栈中申请的,如果函数结束那么对象的空间将会自动的适当,此时返回的引用是一个指向无用空间的引用,一旦对这个引用进行使用将会出现不确定的结果!还有一种情况是返回的引用的对象是在函数的堆中申请的,如下:
const Ration& operator*(const Ration& lhs,const Ration& rhs){ Ration result = new Ration(lhs.n * rhs.n, lhs.b * rhs.b); return result; }在这种情况下如果不对new的空间进行内存的释放,返回的引用指向的空间都是有效的,但是容易产生的一个问题是内存泄露的情况,如果有计算:Ration result = (a*b)*c,其中a、b、c都是Ration类型的对象,此时就会产生内存泄露的问题!
书中还介绍了一种情况就是返回一种static的对象,例如:
const Ration& operator*(const Ration& lhs,const Ration& rhs){ static Ration result(lhs.n * rhs.n, lhs.b * rhs.b); return result; }此时在函数内部返回的是static的对象,这个对于多线程情况下容易发生错误,此外由于对象是函数local static,则这个对像在一个函数内部是只保留一份copy的,那么我们对这个函数的多次调用中指向的result对象都是相同的!例如:
if(a * b == b * c){ ... }上面这个例子中,不管a/b/c是什么样子的数,返回的结果都是真,因为*返回的是一个静态的成员,而该成员在函数中只保留一份!
请记住:
- 绝对不要返回一个ponit或者reference指向一个local stack对象、一个heap对象或者static对象,虽然pass-by-value会造成函数构造和析构的成本,但是至少能保证正确性!
对于C++而已,封装型是其三大特征之一,之所以封装型是如此的重要,是因为封装型能为接口的使用者提供透明的服务,内部的任何改变对其客户使用者来说都是透明的,都不需要做任何的改变,将class中的成员变量设为private的并且为成员变量设定对应的接口函数,这样外面的调用者只能通过接口函数来使用成员变量,对成员变量的任何改动只要接口不变都是可以对客户保持透明不变的!举个简单的例子:
class SpeedDataCollection{ ... public: void addValue(int speed); double averageSoFar() const; .... };这是一个计算速度平均值的类,我们在函数averageSoFar中计算到现在为止收集到的所有速度的平均值,对该接口使用时我们只需要定义一个对象然后调用该接口函数就可以了,我们不用关系数据是怎样收集的怎样计算的,对于不同的要求我们可以改变速度的计算方式或者存储方式,但是对接口使用者来说这些都是透明的看不到的!
假设我们有一个public成员变量,而我们最终取消了它。多少代码可能会被破坏呢?所有使用它的客户码都会被破坏,而那是一个不可知的大量。因此public成员变量完全没有封装性。假设我们有一个protected成员变量,而我们最终取消了它。多少代码可能会被破坏呢?所有使用它的derived classes都会被破坏,那往往也是个不可知的大量。因此,protected成员变量就像public成员变量一样缺乏封装性。从封装性的角度观察之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
请记住:
- 对类的成员变量要采用private的形式。在封装型上public和protected都是不提供封装的,实质上两个没什么区别的!
在C++中封装性是其三大特性之一,本条款可以看作是最大限度的增加class的封装性,在C++中member函数包括friend函数是由对类的成员变量的访问权限,而相对应的non-member函数和non-friend函数都是没有权限的,这样在封装性上肯定是后者对其封装效果好,因此如果一个class的member函数可以用一个non-member函数完整的实现,则尽量的使用non-member函数和non-friend函数,当然这个函数可以是另一个类的成员函数或者friend函数,不过常见的用法是和该类作为同一个namespace的成员。
例如:
namespace WebBrowsStuff{ class webBrowser{...}; void clearBrowser(webBrowser& wb); ... }
在该namespace空间中,clearBrowser类作为一个non-member函数来对class webBrowser来进行处理!其中namespace是可以在不同的源码文件中存在的,这样如果我们对webBrowser处理的另外一个对不同客户使用的non-member函数我们可以放在其他的源码文件中,我们只要能保证namespace的名字相同就行,这样我们就能很方便的对处理函数进行扩展!
请记住:
- 宁可拿non-member 和 non-friend函数来替换member函数,这样做可以增加封装性,包裹弹性和技能扩充性!
条款24:若所有参数皆需要类型转换,请为此采用non-member函数
考虑一个有理数的类:
class Ration{ public: Ration(int numerator = 0, int denominator = 1); int numerator() const; int denominator() const; private: int numerator; int denominator; };
对于不同类型的数中我们希望能够进行隐式的类型转化,例如int型与double类型进行操作时int型会隐式的转化为double类型这里我们也希望能将int型和Ration类型进行类型转化,首先观察下operator*函数:
class Ration{ public: ... const Ration operator* (const Ration& lhs,const Ration& rhs) const; };下面的操作:
Ration oneEight(1,8); Ration oneHalf(1,2); Ration result = oneEight * oneHalf; result = result * oneEight;在当前的这种情况下上面的操作能进行的很好,接下来看下面的操作:
result = oneEight * 2; result = 2 * oneEight;第一个能执行的很好,但是第二个却不能编译通过,对于内置类型中的交换律该类不能很好的支持,如果我们修改成下面的形式我们就能很好的看出来为什么第二个不能支持:
result = oneEight.operator*(2); result = 2.operator*(oneEight);明显的对于第二个,2不是一个Ration类,想对应的也就没有operator*函数,所以失败,在第一个中,在oneEight的operator*操作中需要的是一个Ration的对象,在这里2隐式的转化为了一个Ration对象,因为Raion构造函数有默认值并且不是explicit修饰的!为了能保证支持下面那种情况,我们要把第二种情况下的2也能转化为一个Ration类型,因为只有参数中才能进行隐式类型转化,因此我们也就是将第二种情况下的2也作为参数传入,此时我们将member函数转化为一个non-member函数,如下:
const Ration operator*(const Ration& lhs,const Ration& rhs){ ... }因为此时不是做为一个class的member函数,因此第一个参数也就不会是一个this指针的Ration对象,而是作为一个operator*的第一个参数存在,所以上面的两种情况都能很好的做到支持!
还有人可能想到需不需要把该函数作为该class的friend函数,记住如果non-member和non-friend函数能解决的就不要应用对应的member和friend函数,这个在上一条款中已经说过了!
请记住:
- 如果你需要为一个函数的所有参数进行类型转化(包括this指针所指向的那个隐喻参数),那么这个函数为non-member函数!
条款25:考虑写出一个不抛出任何异常的swap函数