赋值操作符的异常实现方式

  在类的定义中,我们通常会重载赋值操作符,来替代编译器合成的版本,实现中会对每个类的成员变量进行具体的操作,比如下面的代码:

 1 class Sales_Item  2 {  3 public:  4     Sales_Item& operator=(const Sales_Item & rhs);  5 //other mebers and functions
 6 private:  7     char *pIsbn;  8     int units_sold;  9     double revenue; 10 }; 11 
12 Sales_Items& Sales_item::operator=(const Sales_Item & rhs) 13 { 14     if(this != &rhs) 15  { 16         if(pIsbn) 17  delete[] pIsbn; 18         pIsbn = new char[strlen(rhs.pIsbn)+1]; 19  strcpy(pIsbn, rhs.pIsbn); 20         
21         units_sold = rhs.units_sold; 22         revenue = rhs.revenue 23  } 24     return *this; 25 }

  需要先判断是否为同一个对象,再用形参对象中的成员变量对当前对象成员变量进行赋值。类的成员变量涉及到内存、资源的分配时,需要重载赋值操作符,避免内存、资源的泄露和重复释放等问题。在某处看到一个重载赋值操作符定义如下:

1 T& T::operator = (const T& other) 2 { 3     if(this != &other) 4  { 5         this->~T(); 6         new (this) T(other); 7  } 8     return *this; 9 }

  可以看出这个operator=的定义上很简单,首先调用T类的析构函数,然后使用placement new在原有的地址上,以other为形参,调用T类的拷贝构造函数。在这种惯用法中,拷贝赋值运算符是通过拷贝构造函数实现的,它努力保证T的拷贝赋值运算符和拷贝构造函数完成相同的功能,使程序员无需再两个不同的地方编写重复代码。对于Sales_Item类,如果用这个operator=来代替其原有的实现,尽管不会出错,但这种定义是一种非常不好的编程风格,它会带来很多问题:

  • 它切割了对象。如果T是一个基类,并定义了虚析构函数,那么"this->~T();new (this) T(other);" 将会出现问题,如果在一个派生类对象上调用这个函数,那么这些代码将销毁派生类对象,并用一个T对象来代替,这几乎会破坏后面所有试图使用这个对象的代码,考虑如下代码:
     1 //在派生类的赋值运算函数中通常会调用基类的赋值运算函数
     2 Derived& Derived::operator=(const Derived& other)  3 {  4     if(this != &rhs)  5  {  6         Base::operator=(other);  7         //...现在对派生类的成员进行赋值...
     8  }  9 
    10     return *this; 11 } 12 
    13 //本实例中,我们的代码是
    14 class U : public T{/*...*/}; 15 U& U::operator=(const U& other) 16 { 17     if(this != &rhs) 18  { 19         T::operator=(other); 20         //...对U的成员进行赋值... 21         //...但这已经不再是U的对象了,销毁派生类对象,并在派生类内存建立基类对象
    22  } 23 
    24     return *this;    //同样的问题
    25 }

    在U的operator=中,首先调用父类T的operator=,那么会调用"this->T::~T();",并且随后再加上对T基类部分进行的placement new操作,对于派生类来说,这只能保证T基类部分被替换。而更重要的是,在T类型的operator=中,虚函数指针会被指定为T类的版本,无法实现动态调用。如果要实现正确的调用,派生类U的operator=需要定义与父类T的operator=中同样的实现:

     1 U& operator=(const U& rhs)  2 {  3     if(this != &rhs)  4  {  5         this->~U();  6         new(this)U(rhs);  7  }  8 
     9     return *this; 10 }
  • 它不是异常安全的。在new语句中将调用T的拷贝构造函数。如果在这个构造函数抛出异常,那么这个函数就不是异常安全的,因为它在最后只销毁了旧的对象,而没有用其他对象来代替。
  • 它改变了正常对象的生存期。根本问题在于,这种惯用法改变了构造函数和析构函数的含义。构造过程和析构过程应该与对象生存期的开始/结束对应,而在通常含义下,此时正是获取/释放资源的时刻。构造过程和析构过程并不是用来改变对象的值得。
  • 它将破坏派生类。调用"this->T::~T();",这种方法只是对派生类对象中"T"部分(T基类子对象)进行了替换。这种方法违背了C++的基本保证:基类子对象的生存期应该完全包含派生类对象的生存期——也就是说,通常基类子对象的构造要早于派生类对象,而析构要晚于派生类对象。特别是,如果派生类并不知道基类部分被修改了,那么所有负责管理基类状态的派生类都将失败。

  测试代码:

 1 class T  2 {  3 public:  4     T(const char *pname, int nage)  5  {  6         name = new char[strlen(pname)+1];  7         strcpy_s(name, strlen(pname)+1, pname);  8         age = nage;  9  }  10     T(const T &rhs)  11  {  12         name = new char[strlen(rhs.name)+1];  13         strcpy_s(name, strlen(rhs.name)+1, rhs.name);  14         age = rhs.age;  15  }  16     T& operator=(const T& rhs)  17  {  18         if(this != &rhs)  19  {  20             cout<<"T&operator="<<endl;  21             this->~T();  22             new(this)T(rhs);  23  }  24 
 25         return *this;  26  }  27     virtual ~T()  28  {  29         if(name!=NULL)  30  delete[] name;  31         cout<<"~T()"<<endl;  32  }  33     virtual void print(ostream& out)const
 34  {  35         out<<"name is "<<name<<", age is "<<age;  36  }  37 private:  38     char *name;  39     int age;  40 };  41 
 42 ostream& operator<<(ostream& out, const T&t)  43 {  44     t.print(out);  45     return out;  46 }  47 
 48 class U:public T  49 {  50 public:  51     U(const char *pname, int nage, const char *prace, int nchampion):T(pname, nage)  52  {  53         race = new char[strlen(prace)+1];  54         strcpy_s(race, strlen(prace)+1, prace);  55         champion = nchampion;  56  }  57     U(const U &rhs):T(rhs)  58  {  59         race = new char[strlen(rhs.race)+1];  60         strcpy_s(race, strlen(rhs.race)+1, rhs.race);  61         champion = rhs.champion;  62  }  63     U& operator=(const U& rhs)  64  {  65         if(this != &rhs)  66  {  67         /* T::operator=(rhs);  68  race = new char[strlen(rhs.race)+1];  69  strcpy_s(race, strlen(rhs.race)+1, rhs.race);  70  champion = rhs.champion;  71             */
 72             this->~U();  73             new(this)U(rhs);  74  }  75 
 76         return *this;  77  }  78     virtual ~U()  79  {  80         if(race!=NULL)  81  delete[] race;  82         cout<<"~U()"<<endl;  83  }  84     virtual void print(ostream& out)const
 85  {  86         T::print(out);  87         out<<", race is "<<race<<", champion number is "<<champion<<".";  88  }  89 private:  90     char *race;  91     int champion;  92 };  93 int _tmain(int argc, _TCHAR* argv[])  94 {  95     cout<<sizeof(T)<<"  "<<sizeof(U)<<endl;  96 
 97     U u("Moon", 21, "Night Elf", 0);  98     U t("Grubby", 21, "Orc", 2);  99 
100     u = t; 101     cout<<u<<endl; 102 
103     return 0; 104 }
View Code

   在重载operator=运算符时,另一个值得关注的是,用const来修饰返回值:

 1 class T  2 {  3 public:  4     T(int x=12):value(x){}  5     const T& operator=(const T & rhs)  6  {  7         if(this != &rhs)  8  {  9             //implement
10  } 11 
12         return *this; 13  } 14     int getValue() 15  { 16         return value; 17  } 18     void setValue(int x) 19  { 20         value = x; 21  } 22 public: 23     int value; 24 }; 25 
26 int main() 27 { 28  T t1; 29  T t2; 30     t2 = t1; 31     t2.setValue(21); 32 
33     return 0; 34 }

   注意setValue函数改变了t2对象的value值,而line26赋值后,t2仍然可以调用setValue函数,这说明“返回const并不意味着类T本身为const,而只意味着你不能使用返回的引用来直接修改它指向的结构”。看看下面这段代码:

1 int main() 2 { 3  T t1; 4  T t2; 5     (t2=t1).setValue(21); 6 
7     return 0; 8 }

   这里直接对t2=t1的返回结果调用setValue,因为返回的是const&类型,所以不能调用此setValue函数。

你可能感兴趣的:(操作符)