设为首页 加入收藏

TOP

8.4.3 重载赋值运算符
2013-10-07 12:30:09 来源: 作者: 【 】 浏览:67
Tags:8.4.3 重载 运算

8.4.3  重载赋值运算符

如果我们不亲自给类提供重载的赋值运算符函数,则编译器将提供一个默认的函数。默认版本仅仅提供逐个成员的复制过程,与默认复制构造函数的功能类似;但是,不要混淆默认复制构造函数与默认赋值运算符。默认复制构造函数是通过声明以现有同类对象进行初始化的类对象,或者通过以传值方式给函数传递对象而被调用的。反之,默认赋值运算符是在赋值语句的左边和右边是同类对象时被调用的。

就CBox类来说,使用默认赋值运算符没有任何问题,但对于那些动态给成员分配空间的类而言,我们就需要仔细考虑这些类的要求。如果我们在此类情形中不考虑赋值运算符,则程序中可能产生混乱。

让我们暂时返回到讨论复制构造函数时使用的CMessage类。记得该类有个成员pmessage,它是指向字符串的指针。现在考虑默认赋值运算符可能产生的结果。假设我们有该类的两个实例motto1和motto2。如下所示,我们可以尝试使用默认赋值运算符,使motto2的成员等于motto1的成员。

  1. motto2 = motto1;        // Use default assignment operator 

为该类使用默认赋值运算符的结果基本上与使用默认复制构造函数相同-- 即灾难降临!因为两个对象都有一个指向相同对象的指针,所以只要修改一个对象的字符串,受影响的就是两个对象。另外一个问题是:当该类的实例之一被销毁时,其析构函数将释放该字符串占用的内存,因此另一个对象包含的指针将指向可能已经被其他对象占用的内存。

我们需要赋值运算符做的事情是将文本复制到目标对象所拥有的内存区域。

修正问题

我们可以使用自己的赋值运算符函数来修正上述问题。该函数被定义在类定义内部:

  1. // Overloaded assignment operator for CMessage objects  
  2. CMessage& operator=(const CMessage& aMess)  
  3. {  
  4. // Release memory for 1st operand  
  5. delete[] pmessage;  
  6. pmessage = new char[ strlen(aMess.pmessage) + 1];  
  7.  
  8. // Copy 2nd operand string to 1st  
  9. strcpy(this->pmessage, aMess.pmessage);  
  10.  
  11. // Return a reference to 1st operand  
  12. return *this;  

这里的赋值看起来非常简单,但几点微妙之处需要进一步深究。注意,我们从赋值运算符函数中返回的是引用。赋值运算符函数确实能够完成赋值操作,赋值运算符右边的对象将被复制到左边。表面上看,返回引用意味着不需要返回任何东西,但我们需要进一步考虑该运算符的使用方式。

我们有时会在表达式的右边使用赋值操作的结果,考虑下面这条语句:

  1. motto1 = motto2 = motto3; 

因为赋值运算符具有右结合性,即首先执行将motto3赋给motto2的操作,所以该语句可翻译成下面这条语句:

  1. motto1 = (motto2.operator=(motto3)); 

此处运算符函数调用的结果在等号的右边,因此该语句最终变为:

  1. motto1.operator=(motto2.operator=(motto3)); 

要使这条语句工作,我们当然必须有返回对象。括弧内对operator=()函数的调用必须返回一个对象作为另一个operator=()函数调用的实参。本例中,返回类型为CMessage或CMessage&都可以,在此类情形中返回引用不是必需的,但我们无论如何都必须返回CMessage对象。

但是,考虑下面的例子:

  1. (motto1 = motto2) = motto3; 

这是完全合法的代码,括弧旨在确保首先执行最左边的赋值。该语句可翻译成下面的语句:

  1. (motto1.operator=(motto2)) = motto3; 

当我们将剩下的赋值操作表示成显式的重载函数调用时,该语句最终变为:

  1. (motto1.operator=(motto2)).operator=(motto3); 

现在的情况是,从函数operator=()返回的对象被用来调用operator=()函数。如果返回类型仅仅是CMessage,则该语句是不合法的,因为实际返回的是原始对象的临时副本,编译器不允许使用临时对象调用成员函数。换句话说,返回类型是CMessage的情况下返回值不是左值。确保此类语句能够正确编译和工作的唯一方法是返回可以作为左值的引用,因此如果希望实现使用赋值运算符处理类对象的灵活性,则唯一可能的返回类型是CMessage&。

注意,本地C++(www.cppentry.com)语言对赋值运算符的形参和返回类型没有任何限制,但如果希望自己的赋值运算符函数支持C++(www.cppentry.com)赋值用法,那么以刚才描述的方式声明赋值运算符就具有现实意义。

我们需要记住的第二点微妙之处是,两个对象都已经拥有为字符串分配的内存,因此赋值运算符函数首先要删除分配给第一个对象的内存,然后重新分配足够的内存,以容纳属于第二个对象的字符串。做完这件事之后,就可以将来自第二个对象的字符串复制到第一个对象现在拥有的内存中。

该运算符函数中仍然存在缺点。如果我们写出下面这条语句,那么将发生什么事情呢?

  1. motto1motto1 = motto1; 

显然,我们不会直接那样做,但此类现象很容易隐藏在指针的背后,就像下面的语句那样:

  1. Motto1 = *pMess; 

如果指针pMess指向motto1,那么实质上这就是前面那条赋值语句。这种情况下,目前的赋值运算符函数将释放供motto1使用的内存,然后基于已经被删除的字符串的长度另外分配一些内存,并试图复制当时很可能已经被破坏的旧内存。通过在函数的开头检查左右操作数是否相同,我们就可以修正上述问题,因此现在operator=()函数的定义将如下所示:

  1. // Overloaded assignment operator for CMessage objects  
  2. CMessage& operator=(const CMessage& aMess)  
  3. {  
  4. if(this == &aMess)      // Check addresses, if equal  
  5. return *this;               // return the 1st operand  
  6.  
  7. // Release memory for 1st operand  
  8. delete[] pmessage;  
  9. pmessage = new char[ strlen(aMess.pmessage) +1];  
  10.  
  11. // Copy 2nd operand string to 1st  
  12. strcpy(this->pmessage, aMess.pmessage);  
  13.  
  14. // Return a reference to 1st operand  
  15. return *this;  

这部分代码假定函数定义出现在类定义内部。

试一试:重载赋值运算符

下面将这些代码放在可运行的示例中,同时还将添加一个Reset()成员函数。该函数的作用仅是将消息重新设置为星号字符串。

  1. // Ex8_05.cpp  
  2. // Overloaded copy operator perfection  
  3. #include <iostream> 
  4. #include <cstring> 
  5. using std::cout;  
  6. using std::endl;  
  7.  
  8. class CMessage  
  9. {  
  10. private:  
  11. char* pmessage;             // Pointer to object text string  
  12.  
  13. public:  
  14. // Function to display a message  
  15. void ShowIt() const  
  16. {  
  17. cout << endl << pmessage;  
  18. }  
  19.  
  20. //Function to reset a message to *  
  21. void Reset()  
  22. {  
  23. char* temp = pmessage;  
  24. while(*temp)  
  25. *(temp++) = '*';  
  26. }  
  27.  
  28. // Overloaded assignment operator for CMessage objects  
  29. CMessage& operator=(const CMessage& aMess)  
  30. {  
  31. if(this == &aMess)              // Check addresses, if equal  
  32. return *this;                   // return the 1st operand  
  33.  
  34. // Release memory for 1st operand  
  35. delete[] pmessage;  
  36. pmessage = new char[ strlen(aMess.pmessage) +1];  
  37.  
  38. // Copy 2nd operand string to 1st  
  39. strcpy(this->pmessage, aMess.pmessage);  
  40.  
  41. // Return a reference to 1st operand  
  42. return *this;  
  43. }  
  44.  
  45. // Constructor definition  
  46. CMessage(const char* text = "Default message")  
  47. {  
  48. pmessage = new char[ strlen(text) +1 ];         // Allocate space for text  
  49. strcpy(pmessage, text);                     // Copy text to new memory  
  50. }  
  51.  
  52. // Destructor to free memory allocated by new  
  53. ~CMessage()  
  54. {  
  55. cout << "Destructor called."                   
    // Just to track what happens  
  56. << endl;  
  57. delete[] pmessage;      // Free memory assigned to pointer  
  58. }  
  59. };  
  60.  
  61. int main()  
  62. {  
  63. CMessage motto1("The devil takes care of his own");  
  64. CMessage motto2;  
  65.  
  66. cout << "motto2 contains - ";  
  67. motto2.ShowIt();  
  68. cout << endl;  
  69.  
  70. motto2 = motto1; // Use new assignment operator  
  71.  
  72. cout << "motto2 contains - ";  
  73. motto2.ShowIt();  
  74.  
  75. cout << endl;  
  76.  
  77. motto1.Reset();     // Setting motto1 to * doesn't  
  78.                                             // affect motto2  
  79.  
  80. cout << "motto1 now contains - ";  
  81. motto1.ShowIt();  
  82. cout << endl;  
  83.  
  84. cout << "motto2 still contains - ";  
  85. motto2.ShowIt();  
  86. cout << endl;  
  87.  
  88. return 0;  

从该程序的输出可以看出,一切都完全按照要求工作,两个对象的消息之间没有任何联系,除非我们显式地使它们相等:

  1. motto2 contains -  
  2. Default message  
  3. motto2 contains -  
  4. The devil takes care of his own  
  5. motto1 now contains -  
  6. *******************************  
  7. motto2 still contains -  
  8. The devil takes care of his own  
  9. Destructor called.  
  10. Destructor called. 

由此我们得到另一条黄金规则:

如果需要给类的数据成员动态分配空间,则必须实现赋值运算符。

实现赋值运算符之后,在+=这样的操作中将发生什么事情呢?除非我们实现这样的运算符,否则它们不能工作。对于希望用来处理类对象的每种op=形式,我们都需要编写另一个运算符函数。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇8.4.4 重载加法运算符 下一篇8.6.3 实现CBox类(7)

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: