Item 10: Have assignment operators return a reference to *this
其实这一点对于有一定编程经验的人都是熟稔于心,就是在类重载赋值运算符的函数返回类型应该写成类的引用类型,对应的return语句也应该写成return *this;
书上说这只是个协议,没有进行深入的解析,我倒是想在这里做一点深入。
首先 我们都应该知道c++的值返回类型函数和引用返回类型函数的区别,来看个例子就能明白
int func()
{
return 1;
}
int i = func();
对于函数func,在函数执行结束的时候,由于返回类型是值类型,所以会创建一个临时的对象1将它返回,这个对象由于是用完马上销毁的,也就是常说的右值,所以编译器认为它是个const类型,然后这个对象的值再赋值给i 赋值结束之后销毁。
所以实际上值返回发生了一次额外的构造销毁过程,而且值得注意的是这个返回值是一个右值。所以对函数返回的对象进行赋值是错误的。比如下面的例子 func返回类型是指针,在VS中是编译不通过的
#include<iostream>
using namespace std;
int* func(int* x) {
cout << *x << endl;
*x = 20;
return x;
}
int main() {
int a = 10;
int b = 5;
int* p = &a;
func(p) = &b;
cout << *p << endl;
return 0;
}
但如果是引用类型 就会输出 10 和 20
#include<iostream>
using namespace std;
int*& func(int* x) {
cout << *x << endl; // x 和 p 都指向 a 打印输出10
*x = 20; // x 指向的值发生改变 变为20 由于 x 指向 a 所以 a 的值变成20
return x; // 返回的是引用 于是直接返回这个指针x
}
int main() {
int a = 10;
int b = 5;
int* p = &a; // p 指向 a
func(p) = &b; // func返回的是指针x 修改它的指向,从而指向b
cout << *p << endl; // 由于在func中改变了a的值,而p是指向a的 所以打印20
return 0;
}
这个例子明确了值类型和引用类型的左值和右值的问题,但是返回引用的更重要的意义还是为了减少不必要的构造和析构开销,同时保证赋值的语义。比如下面的代码:
#include<iostream>
using namespace std;
class Foo{
public:
Foo(int num){
value = num;
}
Foo(const Foo& other) {
value = other.value;
cout << "copy construction" << endl;
}
~Foo() {
cout << "delete" << endl;
}
Foo operator= (const Foo& other) {
value = other.value;
return *this;
//注意 对于返回类型不是引用的重载=运算符 此时会对调用对象(比如a=b 调用对象就是a)进行一次拷贝作为返回
}
int getValue()
{
return value;
}
private:
int value;
};
int main() {
Foo a(1), b(2), c(3);
// 当执行a=b的时候,a会调用operator= 在operator=中会完成对b的值进行拷贝给a
// 但是由于返回类型是值类型,所以会对a进行一个拷贝,得到一个临时对象并将它返回
// 由于只是完成赋值的操作,没有后续使用这个返回的对象,直接就发生了对临时对象的析构
a = b;
cout << a.getValue() << endl;
return 0;
}
结果很明显,在执行 a = b结束的时候,对a进行了一次拷贝得到一个临时对象进行返回,所以调用拷贝构造函数,紧接着析构这个临时对象,触发析构函数。后面打印的部分就不再多解释。
细心的读者可能就发现一个问题,在Foo operator=中,由于对this*指向的对象(即a)进行了修改,已经完成了我们的目的,那为何不改成void返回类型,不写return语句,这样不就能避免发生这种临时的拷贝了吗?
其实这是合理的,修改代码如下:
#include<iostream>
using namespace std;
class Foo{
public:
Foo(int num){
value = num;
}
Foo(const Foo& other) {
value = other.value;
cout << "copy construction" << endl;
}
~Foo() {
cout << "delete" << endl;
}
void operator= (const Foo& other) {
value = other.value;
//return *this; 不再返回
}
int getValue()
{
return value;
}
private:
int value;
};
int main() {
Foo a(1), b(2), c(3);
a = b;
cout << a.getValue() << endl;
return 0;
}
确实,这样一来保证了不用发生额外的拷贝操作,同时也达到了修改a的值的目的。
但是这会引来一个新的问题,就是在进行连续赋值的时候会发生错误。比如说下面的代码
int a = 1, b = 2, c = 3;
a = b = c
众所周知赋值运算符的右结合性会使得a = b = c的意义等同于 a = (b = c), 分析一下,过程是c赋值给b之后,即完成括号里的部分,会返回b的引用进行对a的赋值,也就是说赋值运算符是有返回值的。
回到刚才的例子,如果我们改成了void类型的赋值运算符重载,那么就不能完成 a = b = c这样的连续赋值了。可以拿代码试一下,是无法通过编译的。
error: no match for ‘operator=’ (operand types are ‘Foo’ and ‘void’)|
所以,最好的解决办法,就是在重载赋值运算符的函数中返回调用对象的引用,这也就是ITEM10的标题,代码如下:
#include<iostream>
using namespace std;
class Foo{
public:
Foo(int num){
value = num;
}
Foo(const Foo& other) {
value = other.value;
cout << "copy construction" << endl;
}
~Foo() {
cout << "delete" << endl;
}
Foo& operator= (const Foo& other) {
value = other.value;
return *this;
//返回引用 不发生拷贝
}
int getValue()
{
return value;
}
private:
int value;
};
int main() {
Foo a(1), b(2), c(3);
//由于返回类型为Foo&,可以完成连续赋值
a = b = c;
cout << a.getValue() << endl;
return 0;
}
运行结果: