EffectiveC++学习笔记-条款11
2017-06-21 07:55
363 查看
条款11 在operator=中处理自赋值
自我赋值发生在对象被赋值给自己时:
这是合法的!当然可能太明显了。
但是这样:
也是都有自我赋值的可能的。
下面举个例子来分析,假如你建立一个class用来保存一个指针指向的一块动态分配的位图。
当然上面代码有很大问题,我们慢慢分析,假如*this和rhs是同一个对象,则相当于让pb指向一个已经被删除的对象。
这可能是比较常用的方法,比如很多书籍上都是这么书写的。但是这样仍具有“异常安全性”问题。
复制代码
大致思路就是删除指针所指向的旧内容,而后再用这个指针指向一块新的空间,空间的内容填充s.p所指向的内容。但有两件事会导致这段代码崩溃,其一就是本条款所说的“自我赋值”。读者不妨想想看,如果
在赋值语句执行时,检测到obj.p已经有指向了,此时会释放掉obj.p所指向的空间内容,但紧接着下一句话就是:
注意*s.p会导致程序崩溃,因为此时s.p也就是obj.p,对其取值*obj.p(根据优先级,这相当于*(obj.p)),obj.p已经在前一句话被释放掉了,所以这样的操作会有bug。
这有两种思路,书上先给出了这样的:
大致的思路是保存好旧的,再试着申请新的,若申请有问题,旧的还能保存。这里可以删掉第一句话,因为“让operator具备异常安全往往自动获得自我赋值安全的回报”。
假如关心效率,可以把测试相等放回去,但是真的考虑效率就要在考虑一下,“自我赋值”的发生频率,因为加上测试相等会让代码变大,而且判断语句,prefetching、caching和pipelining等指令效率都会降低。
还有一种思路,就是先用临时的指针申请新的空间并填充内容,没有问题后,再释放到本地指针所指向的空间,最后用本地指针指向这个临时指针,像这样:
上述两种方法都是可行,但还要注意拷贝构造函数里面的代码与这段代码的重复性,试想一下,如果此时对类增加一个私有的指针变量,这里面的代码,还有拷贝构造函数里面类似的代码,都需要更新,有没有可以一劳永逸的办法?
这样把负担都交给了拷贝构造函数,使得代码的一致性能到保障。如果拷贝构造函数中出了问题,比如不能申请空间了,下面的swap函数就不会执行到,达到了保持本地变量不变的目的。
一种进一步优化的方案如下:
注意这里去掉了形参的引用,将申请临时变量的任务放在形参上了,可以达到优化代码的作用。
自我赋值发生在对象被赋值给自己时:
class Widget{} Wdiget w; w = w;
这是合法的!当然可能太明显了。
但是这样:
a[i] = a[j]; *px = *py;
也是都有自我赋值的可能的。
下面举个例子来分析,假如你建立一个class用来保存一个指针指向的一块动态分配的位图。
class Bitmap{...}; class Wdiget { private: Bitmap* pb; } //operator=的实现 Widget& Widget::operator=(const Widget& rhs) { delete pb; pb = new Bitmap(*rhs,pb); return *this; }
当然上面代码有很大问题,我们慢慢分析,假如*this和rhs是同一个对象,则相当于让pb指向一个已经被删除的对象。
阻止自我赋值
阻止这种事情发生的方法是:Widget& Widget::operator=(const Widget& rhs) { if(this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs,pb); return *this; }
这可能是比较常用的方法,比如很多书籍上都是这么书写的。但是这样仍具有“异常安全性”问题。
一个小示例
我们举个示例具体说一下: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。
解决异常安全性问题
这样做确实能解决上面所说的第一问题:自我赋值。事实上还可能出现另一个问题导致代码崩溃,试想,如果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具备异常安全往往自动获得自我赋值安全的回报”。
假如关心效率,可以把测试相等放回去,但是真的考虑效率就要在考虑一下,“自我赋值”的发生频率,因为加上测试相等会让代码变大,而且判断语句,prefetching、caching和pipelining等指令效率都会降低。
还有一种思路,就是先用临时的指针申请新的空间并填充内容,没有问题后,再释放到本地指针所指向的空间,最后用本地指针指向这个临时指针,像这样:
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; }
上述两种方法都是可行,但还要注意拷贝构造函数里面的代码与这段代码的重复性,试想一下,如果此时对类增加一个私有的指针变量,这里面的代码,还有拷贝构造函数里面类似的代码,都需要更新,有没有可以一劳永逸的办法?
最终方案
本书给出了最终的解决方案:4000 SampleClass& operator= (const SampleClass& s) { SampleClass tmp(s); swap(*this, tmp); return *this; }
这样把负担都交给了拷贝构造函数,使得代码的一致性能到保障。如果拷贝构造函数中出了问题,比如不能申请空间了,下面的swap函数就不会执行到,达到了保持本地变量不变的目的。
一种进一步优化的方案如下:
SampleClass& operator= (const SampleClass s) { swap(*this, s); return *this; }
注意这里去掉了形参的引用,将申请临时变量的任务放在形参上了,可以达到优化代码的作用。
相关文章推荐
- EffectiveC++学习笔记-条款7
- EffectiveC++学习笔记-条款13
- EffectiveC++学习笔记-条款8
- EffectiveC++学习笔记-条款28|29
- EffectiveC++学习笔记-条款32|33
- EffectiveC++学习笔记-条款41|42
- EffectiveC++学习笔记-条款10
- Effective c++学习笔记——条款11:在operateor=中自我赋值
- EffectiveC++学习笔记-条款26|27
- EffectiveC++学习笔记-条款15|16|17
- EffectiveC++学习笔记-条款45
- EffectiveC++学习笔记-条款12
- EffectiveC++学习笔记-条款20|21
- EffectiveC++学习笔记-条款24|25
- EffectiveC++学习笔记-条款49
- Effective c++学习笔记——条款11:在operateor=中自我赋值
- EffectiveC++学习笔记-条款14
- EffectiveC++学习笔记-条款22|23
- EffectiveC++学习笔记-条款34|35
- EffectiveC++学习笔记-条款46