C++学习笔记----7、使用类与对象获得高性能(二)---- 理解对象生命周期(7)
13、对象赋值
就像在c++中可以将一个int值赋给另一个,也可以将一个对象赋值给另一个对象。例如,下面的代码将MyCell的值赋值给anotherCell:
SpreadsheetCell myCell { 5 }, anotherCell;
anotherCell = myCell;
你可能会说是myCell被“拷贝”给了anotherCell。然而,在c++的世界里,“拷贝”只会发生在对象被初始化时。如果一个对象已经有值了被覆写,更准确的词是“赋值”。注意c++提供给拷贝的工具是拷贝构造函数。因为是构造函数,只能用于对象生成,而不能用于对象的赋值。
因此,c++提供了另外一个成员函数在每个类中执行赋值。这个成员函数叫做赋值操作符。它的名字叫做operator=,因为它实际上是对=操作符的重载。在前面的例子中,对anotherCell调用赋值操作符,用myCell作为参数。
在这里解释的赋值操作符有时候也叫做拷贝操作符,因为数据是从右边的对象拷贝到左边的对象。我们后面会讨论另外一种赋值操作符,move赋值操作符,数据是移动而不是拷贝,在特定使用场景下提高了性能。
按惯例,如果你不写自己的赋值操作符,c++会为你写一个允许对象被赋值给另一个对象。缺省的c++赋值行为几乎与缺省的拷贝行为一模一样:从源到目标递归赋值每一个数据成员。
13.1、声明赋值操作符
下面是SpreadsheetCell类的赋值操作符:
export class SpreadsheetCell
{
public:SpreadsheetCell& operator=(const SpreadsheetCell& rhs);// Remainder of the class definition omitted for brevity
};
赋值操作符通常会用常量引用给到源对象,与拷贝构造函数差不多。在这种情况下,源对象叫做rhs,代表等号右边的,当然了,你可以给它起任何名字。赋值操作符被调用的对象是等号左边的。
与拷贝构造函数不一样的是,赋值操作符返回一个对SpreadsheetCell对象的引用,原因是赋值可以连续,如下所示:
myCell = anotherCell = aThirdCell;
这行代码被执行时,发生的第一件事是对anotherCell赋值操作符被用aThirdCell作为其“右边”参数进行调用。接着,myCell的赋值操作符被调用。然而,它的参数不是anotherCell;它的右边是将aThirdCell赋值给anotherCell的结果。等号只是真实成员函数调用的简化。如果你看这儿显示的函数语法,就可以看出问题:
myCell.operator=(anotherCell.operator=(aThirdCell));
现在,可以看出operator=从anotherCell调用必须返回一个值,该值传递给了对myCell的operator=的调用。返回的正确值是对于anotherCell自身的引用,所以它可以作为myCell赋值的源。
警告:实际上可以声明赋值操作符返回想要的任何类型,包括void。然而,总要返回对象的引用,因为客户期待其被调用。
13.2、定义赋值操作符
赋值操作符的实现与拷贝构造函数类似,但也有一些重要的差异。首先,拷贝构造函数只在初始化时调用,所以目标对象还没有有效值。赋值操作符可以覆写对象的当前值。这种考虑直到在对象中动态分配了如内存这样的资源时才会出现,以后会详细讨论。
其次,在C++中给对象自身赋值是合法的。例如,下面的代码能够正常编译与运行:
SpreadsheetCell cell { 4 };
cell = cell; // Self-assignment
赋值操作符需要把自我赋值的可能性考虑进去。在SpreadsheetCell类中,这不重要,因为它只有一个原始的数据类型double。然而,当类中有动态分配内存或者其它资源时,首先要考虑的就是自我赋值问题了,这个我们会在以后讨论。为了避免这样的问题,赋值操作符的首要任务通常是检测自我赋值,如果答案为是的话立马返回。
下面是SpreadsheetCell类的赋值操作符的定义的起头:
SpreadsheetCell& SpreadsheetCell::operator=(const SpreadsheetCell& rhs)
{if (this == &rhs) {
第一行检测是否是自我赋值,但是可能有一点绕。自我赋值发生在等号左边与右边相同的时候。一个方法是指出两个对象是否相同看它们是否占用了相同的内存地址--显式地,如果指向它们的指针是相同的。记得this是一个从任何成员函数对对象进行调用指向可访问对象的指针。这样的话,this就是一个指向左边对象的指针。同样的,&rhs是一个指向右边对象的指针。如果这些指针相同,赋值一定是自我赋值,但是,因为返回类型是SpreadsheetCell&,也要返回正确的值。所有的赋值操作符像下面一样返回*this,自我赋值的情况也不例外:
return *this;
}
this是一个指向成员函数执行的对象的指针,所以*this就是对象本身。编译器返回该对象的引用来匹配声明的返回类型。现在,如果它不是自我赋值的话,你就要给每一个成员进行赋值了:
m_value = rhs.m_value;return *this;
}
这儿成员函数拷贝值,最终,如前面解释的那样,返回*this。
细心的读者会注意到在拷贝赋值操作符与拷贝构造函数之间有一些代码重复;它们都需要拷贝所有的数据成员。我们后面会介绍拷贝且交换的方法来避免这样的代码重复。
注意:SpreadsheetCell赋值操作符这里展示的只是为了演示的目的。实际上,在这种情况下,赋值操作符可以省略,因为缺省编译器生成的已足够好;它会进行简单的成员感知的所有数据成员的赋值。然而,在特定的情况下,这种编译器生成的赋值操作符是不够的,这些情况我们以后会讨论到。
警告:如果类要求对拷贝操作的特殊处理,要实现拷贝构造函数与拷贝赋值操作符。
13.3、显式缺省与删除赋值操作符
可以显式缺省与删除编译器生成的赋值操作符如下:
SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = default;
SpreadsheetCell& operator=(const SpreadsheetCell& rhs) = delete;