2.赋值和公有数据
和复制构造函数一样,如果我们没有声明赋值操作符函数,c++会自动为我们合成一个。缺省的赋值操作符的适用场合和缺省的复制构造函数一样,如果缺省的复制构造函数是错误的,那么缺省的赋值操作符几乎也可以被确定为错误的(反之亦然)。
赋值操作符和复制构造函数都有着几乎同样的逻辑,它们的区别主要在于:
1.当对象被自赋值时,赋值操作符必须可以工作
2.每次对赋值操作符的调用都会改写一个已经存在了的值,如果在被改写的对象中使用了对象外的资源,那么我们可能需要释放这些资源
3.赋值操作符可以向外返回一个值
如下是String类的复制构造函数和赋值操作符:
String::String(const String& s):data(new char[strlen(s.data)+1])
{
strcpy(data, s.data);
}
const String& String::operator=(const String& s)
{
if(&s != this){
delete [] data;
data = new char[strlen(s.data)+1];
strcpy(data, s.data);
}
return *this;
}
值得注意的是上面中的&s和this的比较,如果没有这个比较的话,对于String的自赋值,我们会先删除String中的数据,然后又试图对这些刚被删除的数据进行拷贝,明显是不行的。还有一个值得注意的是:一旦我们确信正在处理两个不同的对象,赋值操作符必须将原有值所控制的资源释放掉(代码中delete的作用)。如果他们中有一个共有逻辑,我们通常会提取出来放到一个私用的成员函数中,并在复制构造函数和赋值操作符中去调用这个共有逻辑。
2.1关于operator=的返回值
赋值操作符应该返回一个被赋值对象的常量引用。这使得用户可以写出如下的代码:
Complex x,y;
x=y=Complex(0,0);
被重载的操作符和内建的操作符有同样的优先级和结合顺序,=是右结合的。在实际中,即使我们没有使用到返回值,通常也会让赋值操作符返回一个值,这在大部分情况下,对性能不会带来什么影响。
3.公用数据
如下一个复数类:
class Complex
{
public:
double real_d;
double imag_d;
Complex(double r, double i):real_d(r), imag_d(i){}
};
这个类可能可以正常工作,但有着非常大的缺陷,问题就是那两个公有的数据成员,可以被用户任意修改!
所以我们在一开始就应该避免使用这样的公用数据,如下:
class Complex
{
private:
double real_d;
double imag_d;
public:
Complex(double r, double i):real_d(r), imag_d(i){}
double real() const {return real_d;}
double imag() const {return imag_d;}
void real(double r) {real_d = r;}
void imag(double i) {imag_d = i;}
};
3.1表示不变量
公用数据还会使我们的类很难用来保证表示不变量。如一个有理数的类Rational:
class Rational
{
private:
int num_d;
int denom_d;
public:
Rational(int n, int d);
int num() const {return num_d;}
int denom() const {return denom_d;}
void num(int n) {num_d = n;}
void denom(int);
};
static void check_zero(int d)
{
if(d == 0){
cerr << "zero denominator in Rational\n";
abort();
}
}
Rational::Rational(int n, int d):num_d(n),denom_d(d)
{
check_zero(d);
}
void Rational::denom_d(int d)
{
check_zero(d);
denom_d = d;
}
如上,我们在Rational的其它成员函数中,我们就不必担心分母出现0的情况,就能保证不变量的成立。得出结论:避免公用数据成员的出现。公用成员将是得我们很难去更改类的实现(如改变信息的存储格式或存储位置);它也无法保证表示不变量的长期有效性。