IT编程入门指导
所谓的自我赋值,指得就是一个对象赋值给自己的简单行为,但这种看起来人畜无害动作,在某些情形下却可能会使得你的代码崩溃。
自我赋值的语句,就像这样:
Widget w; w = w;
很明显,这是一段愚蠢的代码。但既然我们提到自我赋值会引发问题,那我们先来澄清一下自我赋值的情况其实有时并不是那么显而易见的,并不一定都像上述代码那么愚蠢,它们还可能是这样:
a[i] = a[j]; *px = *py; class base { ... }; class derived : base { ... }; void f(derived *p, base &r);
以上代码中,a[i] 和 a[j] 有可能是同一对象,两个不同的指针 px 和 py 有可能指向同一对象,而基类引用 r 也完全有可能引用了指针 p 所指向的同一对象。它们这看起来,要比前面的代码隐蔽多了。
下面来说说,为什么自我赋值会有危险。考虑一个储存了一张 Jpeg 图片数据的类:
class Image { ... ... private: Jpeg *p; };
下面是 Image 类的 operator=() 的实现代码,看起来合情合理:
Image &operator=(const Image &r) { delete p; p = new Jpeg( *r.p ); return *p; }
但,如果 r 跟调用对象是同一对象时,那将意味着在执行 delete p 之时就已经将 r 的图像数据删除了,此时再去根据此数据 new 一个新对象将会引发错误。纠正这个错误也不难,只要加个简单的判断:
Image &operator=(const Image &r) { if( *this == r ) // 自我检测 return; delete p; p = new Jpeg( *r.p ); return *p; }
这的确解决了所谓 “自我赋值安全性” 问题,但随之而来还有另一个问题,那就是 “异常安全性” 问题,假设程序在分配堆内存时,不巧发生了始料未及的错误,也就是 new 语句发生了异常,此时因为 原先对象的图像数据 p 已经被删除,因此这个赋值运算将会导致一个尴尬的结局:新的数据尚未被正常赋予,旧的数据已经被匆匆删除。
因此,我们还需要仔细打磨以上代码,可以将之修改为:
Image &operator=(const Image &r) { Jpeg *tmp = this->p; p = new Jpeg( *r.p ); delete tmp; return *this; }
此时,如果 new 语句再次发生异常,将不会对任何数据造成影响,可以免除编写 自我检测 代码。当然,如果恰巧确实发生了 自我赋值 事件,那么代码将会白白浪费时间创建了一个原图像的复制品,然后让指针指向新的复制品上。如果你很在乎这个事情,你可以将 自我检测 代码重新加到代码中,可是这又将增加程序的尺寸,引入了一个新的结构分支,prefetching、caching 和 pipelining 指令的效率都会被拖累。因此你需要权衡这二者中的利弊。
总结:
- 编写 operator=() 函数时要格外注意操作数是否是同一对象。
- 需要格外注意会发生异常(尤其是堆内存申请的代码)的代码处,是否会导致程序逻辑的不一致性。
- 保证任何函数在同时操作多个对象时,哪怕有多个对象是同一对象的情况下也能正常执行。