8.4.3 重载赋值运算符
如果我们不亲自给类提供重载的赋值运算符函数,则编译器将提供一个默认的函数。默认版本仅仅提供逐个成员的复制过程,与默认复制构造函数的功能类似;但是,不要混淆默认复制构造函数与默认赋值运算符。默认复制构造函数是通过声明以现有同类对象进行初始化的类对象,或者通过以传值方式给函数传递对象而被调用的。反之,默认赋值运算符是在赋值语句的左边和右边是同类对象时被调用的。
就CBox类来说,使用默认赋值运算符没有任何问题,但对于那些动态给成员分配空间的类而言,我们就需要仔细考虑这些类的要求。如果我们在此类情形中不考虑赋值运算符,则程序中可能产生混乱。
让我们暂时返回到讨论复制构造函数时使用的CMessage类。记得该类有个成员pmessage,它是指向字符串的指针。现在考虑默认赋值运算符可能产生的结果。假设我们有该类的两个实例motto1和motto2。如下所示,我们可以尝试使用默认赋值运算符,使motto2的成员等于motto1的成员。
- motto2 = motto1; // Use default assignment operator
为该类使用默认赋值运算符的结果基本上与使用默认复制构造函数相同-- 即灾难降临!因为两个对象都有一个指向相同对象的指针,所以只要修改一个对象的字符串,受影响的就是两个对象。另外一个问题是:当该类的实例之一被销毁时,其析构函数将释放该字符串占用的内存,因此另一个对象包含的指针将指向可能已经被其他对象占用的内存。
我们需要赋值运算符做的事情是将文本复制到目标对象所拥有的内存区域。
修正问题
我们可以使用自己的赋值运算符函数来修正上述问题。该函数被定义在类定义内部:
- // Overloaded assignment operator for CMessage objects
- CMessage& operator=(const CMessage& aMess)
- {
- // Release memory for 1st operand
- delete[] pmessage;
- pmessage = new char[ strlen(aMess.pmessage) + 1];
- // Copy 2nd operand string to 1st
- strcpy(this->pmessage, aMess.pmessage);
- // Return a reference to 1st operand
- return *this;
- }
这里的赋值看起来非常简单,但几点微妙之处需要进一步深究。注意,我们从赋值运算符函数中返回的是引用。赋值运算符函数确实能够完成赋值操作,赋值运算符右边的对象将被复制到左边。表面上看,返回引用意味着不需要返回任何东西,但我们需要进一步考虑该运算符的使用方式。
我们有时会在表达式的右边使用赋值操作的结果,考虑下面这条语句:
- motto1 = motto2 = motto3;
因为赋值运算符具有右结合性,即首先执行将motto3赋给motto2的操作,所以该语句可翻译成下面这条语句:
- motto1 = (motto2.operator=(motto3));
此处运算符函数调用的结果在等号的右边,因此该语句最终变为:
- motto1.operator=(motto2.operator=(motto3));
要使这条语句工作,我们当然必须有返回对象。括弧内对operator=()函数的调用必须返回一个对象作为另一个operator=()函数调用的实参。本例中,返回类型为CMessage或CMessage&都可以,在此类情形中返回引用不是必需的,但我们无论如何都必须返回CMessage对象。在重载的=函数内就已经实现了motto3向motto2的赋值,但是=必须要有返回值,防止有连锁赋值
但是,考虑下面的例子:
- (motto1 = motto2) = motto3;
这是完全合法的代码,括弧旨在确保首先执行最左边的赋值。该语句可翻译成下面的语句:
- (motto1.operator=(motto2)) = motto3;
当我们将剩下的赋值操作表示成显式的重载函数调用时,该语句最终变为:
- (motto1.operator=(motto2)).operator=(motto3);
现在的情况是,从函数operator=()返回的对象被用来调用operator=()函数。如果返回类型仅仅是CMessage,则该语句是不合法的,因为实际返回的是原始对象的临时副本,编译器不允许使用临时对象调用成员函数。换句话说,返回类型是CMessage的情况下返回值不是左值。确保此类语句能够正确编译和工作的唯一方法是返回可以作为左值的引用,因此如果希望实现使用赋值运算符处理类对象的灵活性,则唯一可能的返回类型是CMessage&。
注意,本地C++语言对赋值运算符的形参和返回类型没有任何限制,但如果希望自己的赋值运算符函数支持C++赋值用法,那么以刚才描述的方式声明赋值运算符就具有现实意义。
我们需要记住的第二点微妙之处是,两个对象都已经拥有为字符串分配的内存,因此赋值运算符函数首先要删除分配给第一个对象的内存,然后重新分配足够的内存,以容纳属于第二个对象的字符串。做完这件事之后,就可以将来自第二个对象的字符串复制到第一个对象现在拥有的内存中。
该运算符函数中仍然存在缺点。如果我们写出下面这条语句,那么将发生什么事情呢?
- motto1motto1 = motto1;
显然,我们不会直接那样做,但此类现象很容易隐藏在指针的背后,就像下面的语句那样:
- Motto1 = *pMess;
如果指针pMess指向motto1,那么实质上这就是前面那条赋值语句。这种情况下,目前的赋值运算符函数将释放供motto1使用的内存,然后基于已经被删除的字符串的长度另外分配一些内存,并试图复制当时很可能已经被破坏的旧内存。通过在函数的开头检查左右操作数是否相同,我们就可以修正上述问题,因此现在operator=()函数的定义将如下所示:
- // Overloaded assignment operator for CMessage objects
- CMessage& operator=(const CMessage& aMess)
- {
- if(this == &aMess) // Check addresses, if equal
- return *this; // return the 1st operand
- // Release memory for 1st operand
- delete[] pmessage;
- pmessage = new char[ strlen(aMess.pmessage) +1];
- // Copy 2nd operand string to 1st
- strcpy(this->pmessage, aMess.pmessage);
- // Return a reference to 1st operand
- return *this;
- }
这部分代码假定函数定义出现在类定义内部。
http://book.51cto.com/art/201001/176128.htm