类的赋值运算符是允许你使用= 将一个实例分配给另一个实例。比如说:
MyClass c1, c2;
c1 = c2; // assigns c2 to c1
实际上,一个赋值运算符的函数签名有好几种:
(1) MyClass& operator=( const MyClass& rhs );
(2) MyClass& operator=( MyClass& rhs );
(3) MyClass& operator=( MyClass rhs );
(4) const MyClass& operator=( const MyClass& rhs );
(5) const MyClass& operator=( MyClass& rhs );
(6) const MyClass& operator=( MyClass rhs );
(7) MyClass operator=( const MyClass& rhs );
(8) MyClass operator=( MyClass& rhs );
(9) MyClass operator=( MyClass rhs );
注:rhs表示right-hand side,表示运算符右边的运算对象。
这些签名在返回类型和参数类型上有区别。
虽然返回类型可能不是很重要,但是选择参数类型的选择是至关重要的。
(2), (5), 和(8)通过非const参考的方式传递参数。
这种方法不被推荐。因为这些签名的问题是以下代码将无法编译:
MyClass c1;
c1 = MyClass( 5, 'a', "Hello World" ); // assuming this constructor exists
这个就是前面拷贝构造函数里提到的,临时对象作无法通过非常引用方式传递。
这是因为这个赋值表达式的右手边是一个临时的(未命名的)对象,而C++标准禁止编译器通过非静态引用参数传递一个临时对象。
这使得我们只能通过值或常量引用来传递右边的对象。
尽管看起来通过常量引用传递参考传递比通过值传递更高效,但我们将在后面看到由于异常安全的原因,对源对象做一个临时的拷贝是不可避免的,进而通过值传递允许我们少写几行代码。
什么时候需要写一个赋值运算符函数?
首先,你应该明白,如果你没有声明一个赋值运算符,编译器会隐含地生成一个。
这个隐式赋值运算符对源对象的每个数据成员进行赋值,这个和拷贝构造函数一样。
例如,使用上面的类,编译器提供的赋值运算符函数完全等同于:
MyClass& MyClass::operator=( const MyClass& other ) {
x = other.x;
c = other.c;
s = other.s;
return *this;
}
一般来说,任何时候你需要写你自己的自定义拷贝构造函数时,你也需要写一个自定义的赋值运算符,一般是成员变量包含了原始指针,需要深拷贝的情况。
所以说拷贝构造函数和赋值构造函数关系紧密,类似双生。
需要注意的是,默认的拷贝构造函数对每个成员都调用其拷贝构造函数,而赋值操作符对每个每个成员都调用其赋值操作符。
异常安全的代码是什么意思?
在此插播一个关于异常安全的小故事,因为程序员们经常误以为异常处理就是异常安全。
一个修改了一些 "全局 "状态的函数(例如,一个引用
参数,或修改其实例的数据成员的成员函数)被称为是异常安全的。
实例的成员函数),如果它在异常发生时能使全局状态得到很好的定义,就可以说是异常安全的。
在函数中的任何时候被抛出的异常,如果它能使全局状态得到很好的定义,则被称为异常安全。
这到底是什么意思?好吧,让我们举一个相当牵强的(也很老套)的例子。这个类包装了一个用户指定的数据类型的数组。它有两个数据成员:一个指向数组的指针和一组数组中的元素。
template< typename T >
class MyArray {
size_t numElements;
T* pElements;
public:
size_t count() const { return numElements; }
MyArray& operator=( const MyArray& rhs );
};
上面这是一个模板类,那么,给这个模板类定义一个赋值运算符:
template<>
MyArray<T>::operator=( const MyArray& rhs ) {
if( this != &rhs ) {
delete [] pElements;
pElements = new T[ rhs.numElements ];
for( size_t i = 0; i < rhs.numElements; ++i )
pElements[ i ] = rhs.pElements[ i ];
numElements = rhs.numElements;
}
return *this;
}
但这行代码可能有问题:
pElements[ i ] = rhs.pElements[ i ];
这行代码可能会抛出一个异常。
这一行对类型T调用了operator=,这个类型可能是一些用户定义的类型,其赋值运算符可能会抛出一个异常,也许是内存不足(std::bad_alloc)的异常,或其他用户定义的类型的程序员创造的异常。
如果它真的抛出,例如在复制10的第3个元素时,会发生什么?
好吧,函数调用栈返回,直到找到一个合适的处理程序。
同时,我们的对象的状态是什么?嗯,我们重新分配了我们的数组来容纳10个T,但是我们只成功复制了其中的两个。第三个失败了,剩下的七个甚至没有被尝试过被复制。此外,我们甚至没有改变numElements,所以它还是原来的元素个数。很明显,如果我们在这个时候调用 count() 返回元素个数,返回的数字是不对的。
但显然,MyArray的程序员从来没有想过让count()给出一个错误的答案。更糟糕的是,可能还有其他成员函数更加依赖numElements的正确性(甚至达到崩溃的程度)。
这个例子显然是一个等待爆炸的定时炸弹。
这个operator=的实现不是异常安全的:如果在函数的执行过程中出现了异常抛出,我们就无法知道对象的状态是什么。
我们只能假设它处于这样一种糟糕的状态(即它违反了它自己的一些不变性),以至于无法使用。如果该对象是处于不良状态,甚至不可能在不破坏程序的情况下销毁该对象或导致MyArray抛出另一个异常。
我们知道,编译器在运行析构器的同时会返回栈以寻找一个处理程序。如果在返回栈时抛出了一个异常。程序必然会不可阻挡地终止。
如何编写一个异常安全的赋值运算符?
推荐的方法是通过以下方式来写一个异常安全的赋值运算符复制-交换模式。什么是copy-swap模式?简单地说,它是一种两步骤的算法:首先制作一个副本,然后与副本进行交换。下面是我们的异常安全版本的operator=。
template<>
MyArray<T>::operator=( const MyArray& rhs ) {
// First, make a copy of the right-hand side
MyArray tmp( rhs );
// Now, swap the data members with the temporary:
std::swap( numElements, tmp.numElements );
std::swap( pElements, tmp.pElements );
return *this;
}
注意,这里是要对每个成员进行std::swap,指针类型也可以交换成功。
但如果这里改成:
std::swap(tmp, *this);
那就会变成无线递归调用,直到程序异常后停止执行。
因为你这里定义的是赋值操作符,而swap里,也会调用到这个对象的赋值操作,如果直接传入自己的对象,虽然编译没问题,但执行的时候,就会变成递归调用了。
异常处理和异常安全之间的区别的重要性就在这里:我们并没有阻止异常的发生;事实上。
从rhs到tmp使用拷贝构造函数时就可能会发生,因为它将复制T的内容。
但是,如果复制结构确实抛出异常,注意*this的状态并没有改变,这意味着在异常发生时,我们可以保证
我们可以保证*this仍然是正常的,此外,我们甚至可以说,它保持不变。
但是,你说,那std::swap呢?它能不抛出吗?是的,也不是。默认的std::swap<>,定义在<algorithm>中,可以抛出,因为std::swap<>看起来像这样:
template< typename T >
swap( T& one, T& two )
{
T tmp( one );
one = two;
two = tmp;
}
第一行运行T的复制构造函数,它可以抛出;其余几行是赋值运算符,也可以抛出。
然而,如果你有一个类型T,使用默认的std::swap()可能导致T的复制构造函数或赋值运算符抛出异常,那么你就需要为你的类型提供一个不抛出的 swap() 重载。
因为swap()不能返回失败,而且你也不允许抛出异常。
你的swap()重载必须总是成功。 通过要求swap不抛出异常,上面的operator=是安全的:要么对象被完全复制成功,或者左手边的对象保持不变。
现在你会注意到,我们对operator=的实现在第一行代码中就做了一个临时拷贝。既然我们必须做一个拷贝,我们不妨让编译器自动完成,这样我们就可以修改函数的签名,以便通过值(即拷贝)而不是引用来获取右边的内容。而不是通过引用,这样我们就可以省去一行代码。
template<>
MyArray<T>::operator=( MyArray tmp ) {
std::swap( numElements, tmp.numElements );
std::swap( pElements, tmp.pElements );
return *this;
}
参考:
Copy constructors, assignment operators, - C++ Articleshttp://www.cplusplus.com/articles/y8hv0pDG/