本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第3章,第3.4节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。
3.4 赋值操作符
C++面向对象高效编程(第2版)
现在,让我们分析main程序中的语句:
a = b ; // 将一个栈赋值给另一个栈
在该语句中,我们将对象b赋值给对象a,使用赋值操作符完成赋值操作。如果a和b都是简单的整数,无论a中的值是什么,编译器都会用b中的值将其擦写(overwrite)。就是这么简单。但是在该例中,a和b都是我们创建的对象,这意味着由我们负责赋值操作。我们知道TIntStack类对象之间如何进行赋值,并且能够实现赋值操作符。对于任何赋值操作符,都应注意以下几点:
(1)确保对象没有自我赋值(如a = a)。
(2)复用被赋值对象中的资源或销毁它。
(3)从源对象中将待复制内容复制到目的对象。
(4)最后,返回对目的对象的引用。
赋值操作符已在类中声明。TIntStack类的赋值操作符签名如下:
TIntStack& operator=(TIntStack& source) // ①
TIntStack& operator=(const TIntStack& source) // ②```
下面是赋值操作符的实现:
TIntStack& TIntStack::operator=(const TIntStack& source)
{
// 检查自我复制(即a = a)。
// source是被赋值的对象,&source给出对象source的地址,
// ‘this’是被赋值对象的地址。
if (this == &source) {
cout << “Warning: Assignment to self.n”;
return *this;
}
/*
如果源对象栈中的元素数目小于或等于目的对象的大小,则没有任何问题。
我们要做的就是复制栈中相应的元素和_count变量。
但是,如果源对象栈中元素的数目大于目的对象的大小,则必须先删除目的对象_sp中内存,再为其分配新的内存(与源对象栈中元素的数目相等),然后复制所有元素。
*/
if (source._count > this->_size) {
// 源对象中元素的数目大于目的对象的大小,
// 删除_sp中的内存并重新分配内存。
// 见下文解释
delete [] _sp;
this->_size = source._size;
_sp = new int [ this->_size ];
}
// 无论是小于等于还是大于的情况,都会用到以下代码遍历栈,
// 并复制元素
for (int i = 0; i < source._count; i++)
this->_sp[i] = source._sp[i]; // 复制元素
this->_count = source._count;
return *this; // 见下文解释
}`
分析:赋值操作由程序员显式完成,系统(编译器)绝不会直接调用它。赋值是在两个现有对象之间执行的操作。如下语句:
`
a = b;`
当a和b都为对象时,上句可解析为:
`
operator=(b);`
换言之,左侧(left hand side,缩写为LHS)的对象调用成员函数operator=(),右侧(right hand side,缩写为RHS)操作数作为operator=()的参数。赋值操作右侧的对象(该例中为b)不能被修改,该操作的副作用(side effect)是:返回对左侧对象(该例中为a)的引用。因为不能修改右侧的对象(我们只能读取右侧对象并写入左侧对象),所以该对象应作为const实参传递给operator=()函数(如②所示)。然而,如果使用没有const参数的赋值操作符(如①所示),就可以被修改右侧的对象。我们并不推荐使用①这样的赋值操作符,因为类的用户并不希望在赋值操作中改变右侧的对象。由于a和b都是真正的现有对象,因此,将b赋值给a时,a中的值将被b中的值擦写(overwrite)。
记住:
如果我们在类中未提供赋值操作符,编译器会为类生成一个默认赋值操作符(编译器绝不会自动调用它,但确实会生成一个)。但是,这样的默认赋值操作符可能无法满足我们的要求,下一章将对此作详细介绍。
在任何赋值操作中,我们要做的就是将数据从源对象赋值到目的对象。一般而言,这很容易,但在某些情况下会有困难。在TIntStack的示例中,我们需要从源栈赋值到目的栈。如果源栈中的元素数目少于或等于目的栈中的可用空间,赋值操作便非常简单,只需复制相应的栈元素。但是,如果源栈中的元素数目多于目的栈所能持有的数目会怎样?我们不允许这样的操作发生,在打印出错误消息后将从赋值操作返回。不过,这样限制太大。或者,我们可以改变目的栈的大小,使其能容纳所有元素。如果我们接受后者的方案,就需要在目的栈上为源栈的元素分配与源栈大小相等的新内存。这样做之前,我们还要删除目的栈中_sp所指向的现有内存(无法扩展这个内存),这就是上面的赋值操作符中调用delete所完成的任务。一旦完成内存分配,我们就只需要复制相应的元素进栈即可(见图3-3和图3-4)。
(摘自前面的main程序)
TIntStack a(5); // 自动对象(_auto object_),在退出main时被销毁
TIntStack b(20); // 另一个自动对象```
最后,我们返回对目的对象(也就是栈a)的引用。你可能会质疑,为什么要这样做?假设我们编写了一个级联赋值操作(cascaded assignment operation)如下:
`c = a = b; // 将b赋值给a,然后将结果再赋值给c`
赋值操作按从右到左的顺序执行1,此表达式即转换为:
`c = (a = b); // 首先将b赋值给a,然后将结果赋值给c`
警告:
既然可以写“a = b;”,那么也可以写
`a = a; // 语法上正确,但是逻辑上错误!`
这样的表达式可能会通过指针和引用(别名)直接或间接地发生,我们的实现必须检查是否出现这种情况。因此,必须核实源地址和目的地址是否相同。如果赋值发生在相同的对象之间,则不会进行任何操作,只返回对目的对象的引用。这就是上面示例的实现中,进行自我赋值检查所完成的工作。
如果没有进行自我赋值检查,可能会导致严重问题。考虑以下包含指针数据成员的TString类:
class TString {
public:
TString (const char* sp) {
_data = new char[strlen(sp) + 1];
strcpy(_data, sp);
}
TString & operator=(const TString & assign);
private:
char* _data; // 字符串指针
};
TString& TString::operator=(const TString& assign)
{
// 错误代码,未检查自我赋值
delete [] this->_data; // ①
data = new char[strlen(assign.data) + 1]; // ②
strcpy(this->_data, assign._data);
return *this;
}
TString x1(“Text string1”);
x1 = x1;`
以上operator=
的实现并不安全,其中的①,删除的是x1._data。接着在②中,我们又试图使用assign._data。因为左侧的对象(*this)与右侧的对象(assign)是同一个对象,即assign._data与x1._data相同。所以,我们搬起石头砸自己的脚。当执行②时,该程序可能崩溃,或者在别的地方出现问题。因此,在赋值操作的实现中,必须进行自我赋值检查。
图3-4
将b赋值给a非常简单,之前的示例中已介绍过赋值如何工作。我们希望将赋值(b赋值给a)的结果再赋值给c。为使其正常工作,第1个赋值操作应返回a的值(或对a的引用)。否则,从a到c的第2次赋值就会出问题。通常,在C和C++中,我们只知道使用赋值操作的结果,却并不了解内部的实现。例如,正是因为赋值操作的副作用,才使得以下代码可以正常工作:
int i = 10, j = 20; // 此处无赋值操作
int k;
cout << (i = j) << endl; // 将j赋值给i,并打印赋值结果①。
if (k = j) { // 将j赋值给k,如果结果不为0,执行if体内部的语句②。
// 省略此处代码
}```
在①中,完成赋值后返回i的值,并打印该值。与此类似,在②中,将j赋值给k,完成赋值后,返回k的值并与0进行比较。
为了让以上赋值都能正常工作,必须确保在赋值操作完成后,该赋值操作返回对a对象的引用。
`Pascal:`
在类似Pacal的语言中,以下的赋值操作没有副作用:
`a := b;`
在赋值后无法返回a的值。因此,以下语句
`c := a := b;`
无法在Pascal(以及类似的语言,如Modula-2)中使用。
思考:
对比复制构造函数和赋值操作符中的代码,会发现很多相似的地方。它们都将现有对象复制到另一个对象中。但是,你能否发现两处代码的主要差别?换言之,复制构造函数和赋值操作符的代码有何区别?能否让它们共享实现?
为完成TIngStack的示例,其余的成员函数实现如下:
void TIntStack::Push(int what)
{
if (_count < _size) { // 如有更多空间储存元素
_sp[_count] = what; // 储存元素
_count++; // 递增count
}
else {
cout << “Stack is FULL. Cannot Push value” << what << endl;
}
}
int TIntStack::Pop()
{
if (_count <= 0) { // 栈为空
cout << “Stack is EMPTYn”;
exit (1); // 如果失败如何报错?可在此处抛出异常。
}
_count--;
return _sp[_count];
}
unsigned TIntStack::HowMany() const
{
return _count;
}`
1此处用到操作符重载和操作符结合律,将在第8章中介绍。在本章讨论中,只需记住赋值从右向左结合。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。