8.4.3 重载赋值运算符(1)
如果我们不亲自给类提供重载的赋值运算符函数,则编译器将提供一个默认的函数。默认版本只提供逐个成员的复制过程,与默认复制构造函数的功能类似;但是,不要混淆默认复制构造函数与默认赋值运算符。当定义以现有的同类对象进行初始化的类对象,或者通过以传值方式给函数传递对象时,调用默认复制构造函数。另一方面,当赋值语句的左边和右边是同类类型的对象时,调用默认赋值运算符。
就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_s(this->pmessage, strlen(aMess.pmessage) + 1, aMess.pmessage);
- // Return a reference to 1st operand
- return *this;
- }
这里的赋值看起来非常简单,但几点微妙之处需要进一步深究。首先要注意的是,从赋值运算符函数中返回的是引用。这么做的理由似乎并不一目了然--毕竟,赋值运算符函数确实能够完成赋值操作,将复制赋值运算符右边的对象到左边。表面上看,返回引用意味着不需要返回任何东西,但需要进一步考虑该运算符的使用方式。
有时可能需要在表达式的右边使用赋值操作的结果,考虑下面这条语句:
- motto1 = motto2 = motto3;
因为赋值运算符具有右结合性(right-associative),即首先执行将motto3赋给motto2的操作,所以该语句可翻译成下面这条语句:
- motto1 = (motto2.operator=(motto3));
此处运算符函数调用的结果在等号的右边,因此该语句最终变为:
- motto1.operator=(motto2.operator=(motto3));
要使这条语句工作,当然必须有返回对象。括号内对operator=()函数的调用必须返回一个对象作为另一个operator=()函数调用的实参。本例中,返回类型为CMessage或CMessage&都可以,在此类情形中返回引用不是必需的,但无论如何都必须返回CMessage对象。
但是,考虑下面的例子:
- (motto1 = motto2) = motto3;
这是完全合法的代码,括号旨在确保首先执行最左边的赋值。该语句可翻译成下面的语句:
- (motto1.operator=(motto2)) = motto3;
当将剩下的赋值操作表示成显式的重载函数调用时,该语句最终变为:
- (motto1.operator=(motto2)).operator=(motto3);