现在的位置: 首页 > 综合 > 正文

Effective C++学习笔记之第四章(3)

2017年11月21日 ⁄ 综合 ⁄ 共 5312字 ⁄ 字号 评论关闭

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*返回它的引用,那么在创建这个实数对象的时候还是要调用它的构造函数。
2)一个函数创建一个新对象通常只有两种方法:在栈上或者在堆上。栈上的变量都是局部变量。如果根据这种策略下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。对返回一个指向局部变量的指针也是同样的效果。
3)如果是在堆上简历一个对象,然后返回它的引用。这样就需要用到new操作符。这样你还是需要调用一次构造函数,因为使用new操作符申请的内存必须调用一个合适的构造函数来进行初始化。而且这样做有个问题,new了之后,谁来负责delete。像下面这样写就造成了内存泄露。

Rational w, x, y, z;
w = x * y * z;    // same as operator*(operator*(x, y), z)

PS:运算符有结合方向自右向左的只有三类:赋值、单目和三目,其它的都是从左至右结合。
这里用了两次new,那就必须调用两次delete才能将内存清除。但是对于用户来说没有合理的方法让他们来进行delete操作,因为它们根本就没有取得隐藏在引用后面的指针。这必然导致了内存泄露。
4)可能你已经发现了,不管是在栈还是堆上创建一个变量,都必然会调用一次构造函数,而你最初的目标是避免这个构造函数的调用。item4中提供了一个返回局部静态变量的引用是合理的例子(不过那只适用于单线程的情况),所以可能你想到了在函数内部定义一个静态实数对象,然后返回其引用,这样就避免了构造函数的调用。OK,看代码。

//函数定义   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之后,可能连重新编译的这种步骤都可以省了。
3)将成员变量隐藏在函数接口之后可以提供各种各样的实现的灵活性。这样当成员变量被读或者写的时候可以便于通知其他对象、便于验证类的不变量或者函数的前置或者后置条件、可以在多线程中执行控制条件。其实封装性等价于其它语言(Delphi和C#)中的属性,但多了一对小括号。
4)将成员变量封装起来可以保证类的不变量得到维护。并且可以保留日后变更实现的权利。如果不隐藏,以后改变任何public的变量都会很困难,因为那会破坏很多用户的代码。so,public意味着不封装,某种意义上来说,就意味着不可改变,特备是被广泛使用的class而言。
5)这个原则对于protect的成员变量也一样使用。如果一个成员变量是public的,我们改变它,那么所有使用了它的用户代码都必须改变,这个数量不可预知的。同样,如果一个变量是protect的,如果改变它,那么所有继承的派生类的代码都要被改变,这个数量也是不可预知的。所以protect并不比public更具有封装性。这个观点可能会有点违背常理,但是只要将变量声明为protect或者public,如果要改变,就会有大量的代码被重写,重测试,重新写文档以及重新编译。所以从封装的角度来看,访问权限只有两种:private(封装)和其他(不提供封装)。

抱歉!评论已关闭.