我们经常为了方便和偷懒,将内置类型的赋值操作可以写成链式操作。
int x, y, z;
x = y = z = 10;
同样地,我们曾经写过的实现,strcpy,strcat等这些函数,总是会返回一个指针。返回指针的目的也是为了方便我们进行链式操作。
char* mystrcpy(const char* src, char* dst)
{
assert(src != NULL && dst != NULL);
if(*src == *dst)
{
return dst;
}
char* p = dst; //为了返回的字符串指针指向起始位置
while((*dst++ = *src++) != '\0');
return p;
}
如果是我们自定义的类或者类型呢?能不能也像内置类型一样实现链式操作或者连锁赋值呢?
class Widget
{
public:
Widget(){};
Widget& operator=(const Widget& rhs)
{
...
return *this;
}
}
为了实现上面的能够链式的赋值,我们必须将赋值操作符函数返回一个引用,指向操作符的左侧实参。这个规则对于其他的赋值相关的运算都是适用的。
我们再看一看上面实现的strcpy,其中有一段 if 条件判断流。是为了防止我们传入的两个指针指向了同一片内存。这样的问题也就引入下面要说的命题。在赋值操作符函数中处理自我赋值。
Widget w;
w = w;
上面的代码看着蠢不蠢?很蠢,但是他是合法的,既然是合法的,你就不要期望有人不会这样做,当然直接这样写的可能没有,但是很多隐形的情况下是我们不可避免的。比如:
std::vector<Widget> vw;
vw[i] = vw[j];
...
*px = *py;
像上面这些隐形的情况下,自我赋值是很难避免的。造成上面这些并不明显的自我赋值的操作,都是因为别名引起的。
如果我们操作了指针或者引用,而指针和引用被用来指向多个相同类型的对象的时候,就需要考虑这些指针或者引用指向的对象是不是相同的了。
更普遍的,我们都知道,在继承中,base class的指针是可以指向 derived class对的对象的。
class Base{}
class Derived : public Base{}
void func(const Base& pB, Derived* pD){}
上面的例子,我们并不能保证pB和pD指向会不会是同一个对象。
class DataHandle{...}
class Widget
{
public:
Widget& operator=(const Widget& rhs){...}
private:
DataHandle* pData;
}
上面的例子,我们有一个自己的通过对象的方式进行数据资源管理的类,然后在类Widget中有一个指针来管理数据。
Widget& operator=(const Widget& rhs)
{
delete pData;
pData = new DataHandle(*rhs.pData);
return *this;
}
上面这个例子是一个简单版本的赋值操作符的实现,该实现满足了我们前面所说的规则,在赋值操作符中总是返回引用,指向操作符的左侧实参。
但看似简单正常的实现其实是暗藏玄机的,该实现在进行自我赋值的时候,会出现异常。如果当*this 和 rhs是同一个对象的时候,在进行delete pData;
操作的时候,析构掉的不仅仅是this对象中的DataHandle,同时也析构掉了rhs对象的DataHandle。所以就会造成不可挽回的后果。
为了避免上面的问题,我们进行了如下的调整。
Widget& operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pData;
pData = new DataHandle(*rhs.pData);
return *this;
}
这样的修改,在实际应用中是能达到检查自我赋值的目的。但是不可避免的,还是可能会有其他的异常。
假设上述的函数在进行重新申请内存的时候,也就是在执行 pData = new DataHandle(*rhs.pData);
的过程中出现了异常,这样的异常可能是由于内存不足或者DataHandle的拷贝构造函数等抛出的。
但无轮怎样,当new DataHandle出现异常之后,由于我们前面已经析构掉了this对象的pData指针。那么该指针总是会指向一块已经被删除了的DataHandle。我们无法正常的删除指针和读取。
其实我们在前面实现 strcpy函数的时候已经有过一次经验,所以为了避免上面的问题出现,我们是可以借鉴前面的经验的。
Widget& operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
DataHandle* pOrignal = pData;
pData = new DataHandle(*rhs.pData);
delete pOrignal;
return *this;
}
上面的这个实现我们是对原来的DataHandle做了一次备份,删除原来的DataHandle,然后指向新的备份。这个实现甚至可以去掉自我赋值的判断,也是能够正常的处理自我赋值。至于加不加,主要取决于自己判断,是在效率(代码体变大、加入新的控制流分支)和自我赋值发生的频率之间的取舍。
我们在前面学习stl的时候,已经有学到过使用swap函数进行对容器的交换。为了简单,直接并且不用考虑异常的情况下,使用 copy and swap是一件比较简单和轻松的事。唯一要做的就是我们要自己实现swap函数。要保证每一个成员都被复制。
使用copy and swap 既可以避免代码重复,也可以提供强烈的异常保证。
class DataHandle{...}
class Widget
{
public:
void swap(Widget& rhs){...}
private:
DataHandle* pData;
}
Widget& operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
上面的例子中我们使用了by references to const保证提高了效率,同时在函数体内进行了一次拷贝。
当我们使用by value进行传递时,对象的拷贝发生在函数参数的构造阶段。
Widget& operator=(Widget rhs)
{
swap(rhs);
return *this;
}
- 保证赋值操作中总是返回指向*this的引用
- 确保对象在自我赋值的时候能够保证良好的行为
- 确保任何一个函数操作多个对象,并且对象可能为同一个对象时,仍能保持行为正常。