在类的定义中,我们通常会重载赋值操作符,来替代编译器合成的版本,实现中会对每个类的成员变量进行具体的操作,比如下面的代码:
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 }
在重载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函数。