EffectiveC++条款10到12

条款十:令operator=返回一个reference to *this

这个条款很简单的说,先解释一下为什么赋值后还要有返回值,这是因为我们可以这样用连等:

int x, y, z;
 x = y = z;

注意等号是右结合的,所以上述运算实际上等同于:

x = (y = z);

如果没有返回值,上述代码就不能通过编译。

至于为什么返回的是一个引用,这并不是编译器强制的,但有三个原因让你觉得这样做是明智的:

(1) 返回引用可以节省资源,不必要为返回值调用构造函数了;

(2) 形如(x = y) = 3这样的连等,编译器也能接受了,因为这样的写法要求赋值运算符返回的是可以修改的左值;

(3) STL和boost库中的标准代码都是这样写的,为了与它们相兼容,还是放弃你的标新立异吧

类似地,+=、-=等与赋值相关的运算,最好也返回自身的引用。

class Widget {  
public:  
    Widget& operator=(const Widget& rhs) {  
        ...  
        return *this;  
    }  
};  
class Widget {  
public:  
    ...  
    Widget& operator+=(const Widget& rhs) {        // 这个协议适用于+=,-=,*=等等  
        ...  
        return *this;  
    }  
    Widget& operator=(int rhs) {                // 此函数也适用,即使参数类型不符规定  
        ...  
        return *this;  
    }  
};  

一句话:令赋值操作符返回一个reference to *this。

条款十一:在operator=中处理自我赋值

直观的operator=是这样定义的:

class SampleClass
{
private:
         int a;
         double b;
         float* p;
public:
         SampleClass& operator= (const SampleClass& s)
         {
                   a = s.a;
                   b = s.b;
                   p = s.p;
                   return *this;
         }
};

定义了一个WebBrower的类,里面执行对浏览器的清理工作,包括清空缓存,清除历史记录和清除Cookies,现在需要将这三个函数打包成一个函数,这个函数执行所有的清理工作,那是将这个清理函数放在类内呢,还是把他放在类外呢?

如果放在类内,那就像这样:

class SampleClass
{
private:
         int a;
         double b;
         float* p;
public:
         SampleClass& operator= (const SampleClass& s)
         {
                   a = s.a;
                   b = s.b;
                   delete p;
                   p = new float(*s.p);
                   return *this;
         }
};

大致思路就是删除指针所指向的旧内容,而后再用这个指针指向一块新的空间,空间的内容填充s.p所指向的内容。但有两件事会导致这段代码崩溃,其一就是本条款所说的“自我赋值”。读者不妨想想看,如果这样:
SampleClass obj;
obj = obj;
所发生的事情。在赋值语句执行时,检测到obj.p已经有指向了,此时会释放掉obj.p所指向的空间内容,但紧接着下一句话就是:
p = new float(*s.p);
注意*s.p会导致程序崩溃,因为此时s.p也就是obj.p,对其取值obj.p(根据优先级,这相当于(obj.p)),obj.p已经在前一句话被释放掉了,所以这样的操作会有bug。

也许读者不以为意,认为用户不可能傻到会写obj = obj这样的代码出来。事实上也确实如此,明显的错误不大可能会犯,但万一写一个:

SampleClass obj;
 …
SampleClass& s = obj;
…
 s = obj;
 SmapleClass* p = &obj;
 …
 *p = obj;

这种错误就不那么直观了,甚至*pa = *pb也有可能出问题,因为pa与pb大有可能指向的是同一个地址空间。自我赋值一个不小心就会发生,决不要假设用户不用写出自我赋值的语句来。

解决自我赋值只要一句话:

class SampleClass
{
private:
         int a;
         double b;
         float* p;
public:
         SampleClass& operator= (const SampleClass& s)
         {
                   if(this == &s) return *this; // 解决自我赋值的一句话
                   a = s.a;
                   b = s.b;
                   delete p;
                   p = new float(*s.p);
                   return *this;
         }
};

但是,如下程序

class SampleClass
{
private:
         int a;
         double b;
         float* p;
public:
         SampleClass& operator= (const SampleClass& s)
         {
                   if(*this == s) return *this; // 注意条件判断的不同,这样写有问题!
                   a = s.a;
                   b = s.b;
                   delete p;
                   p = new float(*s.p);
                   return *this;
         }
};

这样是不对的,因为==经常是用于对象内每一个成员变量是否相同的判断,而不是地址是否重叠的判断。所以用this == &s才能从地址上来捕捉到是否真的是自我赋值。

这样做确实能解决上面所说的第一问题:自我赋值。事实上还可能出现另一个问题导致代码崩溃,试想,如果p = new float(*s.p)不能正常分配空间怎么办,突然抛出了异常怎么办,这将导致原有空间的内容被释放,但新的内容又不能正常填充。有没有一个好的方法,在出现异常时,还能保持原有的内容不变呢?(可以提升程序的健壮性)

这有两种思路,书上先给出了这样的:

SampleClass& operator= (const SampleClass& s)
{
         if(this == &s) return *this; //可以删掉
         a = s.a;
         b = s.b;
         float* tmp = p; // 先保存了旧的指针
         p = new float(*s.p); // 再申请新的空间,如果申请失败,p仍然指向原有的地址空间
         delete tmp; // 能走到这里,说明申请空间是成功的,这时可以删掉旧的内容了
         return *this;
}

大致的思路是保存好旧的,再试着申请新的,若申请有问题,旧的还能保存。这里可以删掉第一句话,因为“让operator具备异常安全往往自动获得自我赋值安全的回报”。

还有一种思路,就是先用临时的指针申请新的空间并填充内容,没有问题后,再释放到本地指针所指向的空间,最后用本地指针指向这个临时指针,像这样:

SampleClass& operator= (const SampleClass& s)
{
         if(this == &s) return *this; //可以删掉
         a = s.a;
         b = s.b;
         float* tmp = new float(*s.p); // 先使用临时指针申请空间并填充内容
         delete p; // 若能走到这一步,说明申请空间成功,就可以释放掉本地指针所指向的空间
         p = tmp; // 将本地指针指向临时指针
         return *this;
}

上述两种方法都是可行,但还要注意拷贝构造函数里面的代码与这段代码的重复性,试想一下,如果此时对类增加一个私有的指针变量,这里面的代码,还有拷贝构造函数里面类似的代码,都需要更新,有没有可以一劳永逸的办法?

本书给出了最终的解决方案:

SampleClass& operator= (const SampleClass& s)
{
         SampleClass tmp(s);
         swap(*this, tmp);
         return *this;
}

这样把负担都交给了拷贝构造函数,使得代码的一致性能到保障。如果拷贝构造函数中出了问题,比如不能申请空间了,下面的swap函数就不会执行到,达到了保持本地变量不变的目的。

一种进一步优化的方案如下

 SampleClass& operator= (const SampleClass s)
 {
          swap(*this, s);
          return *this;
 }

注意这里去掉了形参的引用,将申请临时变量的任务放在形参上了,可以达到优化代码的作用。

最后总结一下:

(1) 确保当对象自我赋值时operator=有良好的行为,其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap;

(2) 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款十二:复制对象时勿忘其每一个成分

这句话包含两部分的意思:第一部分是要考虑到所有成员变量,特别是后加入的,相应的拷贝构造函数和赋值运算符要及时更新;第二部分是在存在继承时,不要遗忘基类部分的复制。先看第一部分的意思,举个例子:

class SampleClass
{
private:
         int a;
public:
         SampleClass(const SampleClass& s):a(s.a)
         {}
};

这里只举了一个拷贝构造函数的例子,赋值运算符与之类似,如果这个时候又加了一个成员变量,比如double b,拷贝构造函数和赋值运算符就要相应地更新(构造函数当然也要更新,只是构造函数一般不会被忘记,而拷贝构造函数和赋值运算符却常常被遗忘)。像这样:

class SampleClass
{
private:
         int a;
         double d;
public:
         SampleClass(const SampleClass& s):a(s.a),d(s.d)
        {}
};

再看第二部分的意思,当存在继承关系时:

class Derived: public SampleClass
{
private:
         int derivedVar;
public:
         Derived(const Derived& d):derivedVar(d.derivedVar){}
};

像这样,很容易就会漏掉基类的部分,导致基类部分没有得到正常的拷贝,应该修改为如下:

class Derived: public SampleClass
{
private:
         int derivedVar;
public:
         Derived(const Derived& d):SampleClass(d), derivedVar(d.derivedVar){}
};

对于赋值运算符的重载,应该写成这样:

Derived& operator=(const Derived& d)
{
         SampleClass::operator=(d);
         derivedVar = d.derivedVar;
         return *this;
}

可以看到,赋值运算符重载与拷贝构造函数的代码具有很高的相似性,但书上说“不要尝试以某个copying函数实现另一个copying函数”。我觉得这里有争议,上一个条款中,书上已经做到了在赋值运算符中调用拷贝构造函数了,像这样:

Derived& operator=(const Derived& d)
{
         Derived tmp(d);
         swap(*this, tmp);
         return *this;
}

这就是一个在赋值运算符内调用拷贝构造函数的例子,也许在有些情况下,它的效率看上去不那么高,但却为代码的一致性提供了很好的保障,也能有效提供异常安全性。所以,在这个地方,我不大认同书上所说的,如果你有什么想法,可以留言共同讨论。书上所说的定制一个共有的private函数,比如init(),分别在拷贝构造函数和operator=中调用当然也是可以的。

最后总结一下:

Copying函数应该确保复制“对象内的所有成员变量”及“所有基类成分”。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值