在类的定义中,我们通常会重载赋值操作符,来替代编译器合成的版本,实现中会对每个类的成员变量进行具体的操作,比如下面的代码:
class Sales_Item
{
public:
Sales_Item& operator=(const Sales_Item & rhs);
//other mebers and functions
private:
char *pIsbn;
int units_sold;
double revenue;
};
Sales_Items& Sales_item::operator=(const Sales_Item & rhs)
{
if(this != &rhs)
{
if(pIsbn)
delete[] pIsbn;
pIsbn = new char[strlen(rhs.pIsbn)+1];
strcpy(pIsbn, rhs.pIsbn);
units_sold = rhs.units_sold;
revenue = rhs.revenue
}
return *this;
}
需要先判断是否为同一个对象,再用形参对象中的成员变量对当前对象成员变量进行赋值。类的成员变量涉及到内存、资源的分配时,需要重载赋值操作符,避免内存、资源的泄露和重复释放等问题。在某处看到一个重载赋值操作符定义如下:
T& T::operator = (const T& other)
{
if(this != &other)
{
this->~T();
new (this) T(other);
}
return *this;
}
可以看出这个operator=的定义上很简单,首先调用T类的析构函数,然后使用placement new在原有的地址上,以other为形参,调用T类的拷贝构造函数。在这种惯用法中,拷贝赋值运算符是通过拷贝构造函数实现的,它努力保证T的拷贝赋值运算符和拷贝构造函数完成相同的功能,使程序员无需再两个不同的地方编写重复代码。对于Sales_Item类,如果用这个operator=来代替其原有的实现,尽管不会出错,但这种定义是一种非常不好的编程风格,它会带来很多问题:
•它切割了对象。如果T是一个基类,并定义了虚析构函数,那么"this->~T();new (this) T(other);" 将会出现问题,如果在一个派生类对象上调用这个函数,那么这些代码将销毁派生类对象,并用一个T对象来代替,这几乎会破坏后面所有试图使用这个对象的代码,考虑如下代码:
//在派生类的赋值运算函数中通常会调用基类的赋值运算函数
Derived& Derived::operator=(const Derived& other)
{
if(this != &rhs)
{
Base::operator=(other);
//...现在对派生类的成员进行赋值...
}
return *this;
}
//本实例中,我们的代码是
class U : public T{/*...*/};
U& U::operator=(const U& other)
{
if(this != &rhs)
{
T::operator=(other);
//...对U的成员进行赋值...
//...但这已经不再是U的对象了,销毁派生类对象,并在派生类内存建立基类对象
}
return *this; //同样的问题
}
在U的operator=中,首先调用父类T的operator=,那么会调用"this->T::~T();",并且随后再加上对T基类部分进行的placement new操作,对于派生类来说,这只能保证T基类部分被替换。而更重要的是,在T类型的operator=中,虚函数指针会被指定为T类的版本,无法实现动态调用。如果要实现正确的调用,派生类U的operator=需要定义与父类T的operator=中同样的实现:
U& operator=(const U& rhs)
{
if(this != &rhs)
{
this->~U();
new(this)U(rhs);
}
return *this;
}
它不是异常安全的。在new语句中将调用T的拷贝构造函数。如果在这个构造函数抛出异常,那么这个函数就不是异常安全的,因为它在最后只销毁了旧的对象,而没有用其他对象来代替。
它改变了正常对象的生存期。根本问题在于,这种惯用法改变了构造函数和析构函数的含义。构造过程和析构过程应该与对象生存期的开始/结束对应,而在通常含义下,此时正是获取/释放资源的时刻。构造过程和析构过程并不是用来改变对象的值得。
它将破坏派生类。调用"this->T::~T();",这种方法只是对派生类对象中"T"部分(T基类子对象)进行了替换。这种方法违背了C++的基本保证:基类子对象的生存期应该完全包含派生类对象的生存期——也就是说,通常基类子对象的构造要早于派生类对象,而析构要晚于派生类对象。特别是,如果派生类并不知道基类部分被修改了,那么所有负责管理基类状态的派生类都将失败。
测试代码: