zoukankan      html  css  js  c++  java
  • 赋值操作符的异常实现方式

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

     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函数。

  • 相关阅读:
    暑假N天乐【比赛篇】 —— 2019牛客暑期多校训练营(第二场)
    莫比乌斯反演入门解析
    暑假N天乐【比赛篇】 —— 2019牛客暑期多校训练营(第一场)
    暑假N天乐 —— 多重+分组背包及变形
    暑假N天乐 —— 完全背包及变形
    暑假N天乐【比赛篇】 —— 牛客假日团队赛6
    暑假N天乐 —— 01背包及变形
    离线线段树 SPOJ
    [Python]数据类型、常量、变量和运算符(未完待续)
    [Python]从哪里开始学习写代码(未完待续)
  • 原文地址:https://www.cnblogs.com/Tour/p/4132919.html
Copyright © 2011-2022 走看看