chapter 4:设计与声明
item21:必须返回对象时,别妄想返回其reference
1)当程序员了解了pass-by-value的实现效率之后,就会根除其带来的种种邪恶。这样会不可避免的犯下一个致命的错误,那就是传递一个根本不存在的对象的引用。
考虑下面一个例子,假设有一个代表实数的类,里面包含了一个计算两个实数乘积的函数。如下:
class Rational { public: // see Item 24 for why this ctor isn't declared explicit没看到那里,还不知道 Rational(int numerator = 0,int denominator = 1); ... private: int n, d; // numerator and denominator // see Item 3 for why the return type is const //因为如果不是const,那么a*b = 2;是合法的。即使想写的表达式是a*b == 2; friend const Rational operator*(const Rational& lhs, const Rational& rhs); };这里是以值的形式返回。这里就需要考虑对象的构造和析构成本,若非必要,没人会想为这样的对象付出太多代价,问题是需要付出任何代价么?OK,很明显如果以引用的形式返回,那肯定是不需要任何代价的。但是记住引用只是一个已存在的对象的别名而已。在看到声明引用时,应该问自己,这个变量的另外一个名字是什么。如果我们写得这个关于实数的operator *返回一个引用,那就必须是一个已经存在的实数对象的引用。当然你没理由去期望这样一个对象会在你调用operator*之前就存在。就算有这么一个实数对象,然后operator*返回它的引用,那么在创建这个实数对象的时候还是要调用它的构造函数。
//在栈上创建对象的函数代码 warning! bad code! const Rational& operator*(const Rational& lhs,const Rational& rhs) { Rational result(lhs.n * rhs.n, lhs.d * rhs.d); return result; } //在堆上创建对象的函数代码 warning! more bad code! const Rational& operator*(const Rational& lhs,const Rational& rhs) { Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); return *result; }当然这种做法在函数内部还是调用的实数类的构造函数,因为你创建了一个实数类的对象。更糟糕的是,函数返回的是result的一个引用,而result是一个局部变量,在函数退出的时候就被销毁了。这样返回的就是一个被销毁的引用,这绝对会导致一个undefined behavior。对返回一个指向局部变量的指针也是同样的效果。
Rational w, x, y, z; w = x * y * z; // same as operator*(operator*(x, y), z)PS:运算符有结合方向自右向左的只有三类:赋值、单目和三目,其它的都是从左至右结合。
//函数定义 warning! yet more bad code! const Rational& operator*(const Rational& lhs,const Rational& rhs) { static Rational result; // static object to which a reference will be returned result = ... ; // multiply lhs by rhs and put the // product inside result return result; } //假设定义了一个operator==操作如下 bool operator==(const Rational& lhs,const Rational& rhs); //用户以下面方式调用operator* Rational a, b, c, d; ... //相当于if (operator==(operator*(a, b), operator*(c, d))) if ((a * b) == (c * d)) { do whatever's appropriate when the products are equal; } else { do whatever's appropriate when they're not; }如果是这样的话,不管a,b,c,d的值是什么,((a*b) == (c*d))永远都是正确的,甚至它们根本就没用进行比较的动作。因为不管先执行哪个函数,最后静态变量都会是最后执行的操作的结果,因为operator*的所有结果共用了那一个静态变量的内存。好,那你有可能觉得一个静态变量不够,那我定义一个静态变量的数组。好吧,定义一个静态变量的数组,需要先选一个n(数组大小)吧。n太小,会溢出,那跟一个静态变量的情况没什么区别。n太大,会降低程序效率,因为每一个静态变量在第一次构造的时候都需要调用构造函数,这样就需要调用n个构造函数和n个析构函数,但有可能你的函数只被调用了一次。最后想想怎么把值放到这个静态的数组中,这样的代价是什么。最直接的在两个对象之间传值的方法就是赋值,那么赋值操作的代价是什么。对于大多数类型而言,赋值操作的代价跟调用一个析构函数(销毁旧的值)加上一个构造函数(将值拷贝到新变量中)。但是你的目标不是避免调用构造函数和析构函数么?所以这个方法是行不通的。就算你使用STL中的vector容器也不会多这个问题有很大的改善。
inline const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.n * rhs.n, lhs.d * rhs.d); }
所以还是老老实实的让需要返回一个新对象的函数返回一个新对象吧~就像上面的代码一样。这肯定会有构造函数和析构函数的开销,从长远来看这只是为获得正确行为而付出的一个小小代价。万一这里很频繁的调用,会产生很多的实数对象呢?别忘了C++和所有编程语言一样,允许编译器实现者施行最优化,用以改善代码的效率却不改变其可观察的行为。因此某些情况下operator*返回值的构造和析构可被安全的消除(难道意思是有时候没有调用?感觉没懂啊)。
5)作为总结,不要返回一个指向局部变量或者堆变量的pointer或者reference,或者返回一个局部的静态对象(可能需要多个这样的对象)的pointer和reference。当需要决定是返回一个object或者是一个reference的时候,选择行为正确的那个就可以了,其它的关于成本的问题,留给编译器厂商吧。
item22:将成员变量声明为private
1)首先我们知道struct和class一个很大的区别就是struct里面的成员变量默认是public的,而class默认是private的。这种设计引领我们在设计成员变量的时候是不是也应该将变量声明为private。
2)为什么不使用public成员变量呢?首先我们考虑语法的一致性。如果成员变量是非public的,那么访问这个变量的唯一途径就是通过成员函数,这样所有的public接口都是函数,这样在使用成员变量是就不用头疼去记住什么时候有括号,什么时候没有。可能这个语法的一致性还不足以让人信服,OK,但使用函数可以更好的控制对成员变量的处理。如果一个变量是public的,那么每个人都可以去读写它。但是你通过函数去get或者set它,那么你可以实现"不能访问","只读访问","读写访问",甚至只要你愿意,还有"只写"访问。这种细微划分的访问控制很重要,因为很多成员变量都需要隐藏起来,只有少量的成员变量需要一个getter和setter。
//代码示例 class AccessLevels { public: ... int getReadOnly() const { return readOnly; } void setReadWrite(int value) { readWrite = value; } int getReadWrite() const { return readWrite; } void setWriteOnly(int value) { writeOnly = value; } private: int noAccess; // no access to this int int readOnly; // read-only access to this int int readWrite; // read-write access to this int int writeOnly; // write-only access to this int };好吧,如果你还不信服,那只有拿出杀手锏了:封装性啦(个人觉得书的作者语言风格灰常幽默诙谐啊)。如果你通过函数来访问成员变量,那么日后你可以用计算来替换这个成员变量,外部使用者根本就不知道这种变化。举个例子来说明,假设正在写一个汽车自动测速的一个装备。当汽车通过时,把它的速度添加到数据集合中。
class SpeedDataCollection { ... public: void addValue(int speed); // add a new data value double averageSoFar() const; // return average speed ... };现在考虑成员函数averageSoFar的实现。一种方法是使用一个成员变量来保存当前的平均速度,当averageSoFar被调用的时候,返回这个成员变量的值就可以了。另一种方法是每调用一次averageSoFar,就重新计算一次,这需要重新遍历集合中的数据。第一种方法会使SpeedDataCollection对象变得比较大,因为它需要成员变量来保存平均速度、累计的总和、数据点的个数,但是这样averageSoFar的执行效率会很高。相反averageSoFar执行得比较慢,但是SpeedDataCollection对象会比较小。也不能绝对的说哪一个方法更好。如果是那种内存比较紧张的机器上(比如说嵌入式设备),而且不需要频繁的计算平均速度,这样的话第二种方法会比较好一点。但是如果对计算效率要求很高,而内存不是问题的时候,就需要选择第一种方法。当然这里的重点是通过函数来访问成员变量(也就是将成员变量封装起来),这样可以在内部改变它,但是用户需要做的只是重新编译一下就可以了。学习item31之后,可能连重新编译的这种步骤都可以省了。