前言
C++规定,赋值运算符重载只能定义为成员函数。
本文代码在 Linux 系统下用 gcc 测试,并且需要关闭编译器的“返回值优化”,也就是需要加上编译参数 -fno-elide-constructors
。
编译命令:
g++ -std=c++11 -fno-elide-constructors test.cc
C++赋值运算符重载时,总是返回一个引用,为什么?
我们分以下几种情况进行讨论:返回“void”、返回“值类型”、返回“引用类型”、返回“常量引用类型”。
返回“void”
// test.cc
#include <bits/stdc++.h> // Linux 系统
using namespace std;
#define __FILENAME__ \
(strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
struct A {
int x_;
A() { cout << this << "()" << endl; }
A(int x) { cout << this << "(" << x << ")" << endl; }
A(const A& rhs) {
x_ = rhs.x_;
cout << this << "(" << &rhs << ")" << endl;
}
~A() { cout << "~" << this << endl; }
// 返回 void
void operator=(const A& rhs) {
x_ = rhs.x_; // 改变成员变量
}
};
int main() {
A a(1);
A b(2);
b = a; // 等价于 b.operator=(a);
// c = b = a; // 连续赋值,会报错!
}
运算符函数与普通成员函数没有区别,b = a
与 b.operator=(a)
是等价的。也就是传入 a
为 const A&
类型的参数,然后用 a
的成员更新 b
的成员。
可以看到,返回值其实没有作用,因为我们需要的“赋值”操作已经完成了。
返回“值类型”
如果返回 void ,那么如果一条语句存在连续赋值,就会出现问题:
c = b = a;
这条语句等价于:
c = (b = a);
即:
c.operator=(b.operator=(a));
先执行 b.operator=(a)
,返回值是 void,然后将 void 作为参数传递给 c
的成员函数 void operator=(const A&)
,显然参数类型不匹配,找不到这样的函数,所以会报错。
那么,我们尝试返回一个 A
类型的对象,那么返回值就可以作为赋值运算符函数的参数继续调用赋值运算符函数了。
我们给出完整测试程序:
#include <bits/stdc++.h> // Linux 系统
using namespace std;
#define __FILENAME__ \
(strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
struct A
{
int x_;
A()
{
cout << this << "()" << endl;
}
A(int x)
{
cout << this << "(" << x << ")" << endl;
}
A(const A &rhs)
{
x_ = rhs.x_;
cout << this << "(" << &rhs << ")" << endl;
}
~A()
{
cout << "~" << this << endl;
}
// 返回 A 的值类型
A operator=(const A &rhs)
{
x_ = rhs.x_; // 改变成员变量
// return A(*this);
return *this; // 用自身复制构造一个匿名对象返回
}
};
int main()
{
A a(1);
A b(2);
A c(3);
getchar();
b = a; // 等价于 b.operator=(a);
getchar();
c = b = a; // 连续赋值
getchar(); // 暂停,方便观察
}
运行代码,打印如下:
0x7ffcd9143be4(1)
0x7ffcd9143be8(2)
0x7ffcd9143bec(3)
0x7ffcd9143bf0(0x7ffcd9143be8)
~0x7ffcd9143bf0
0x7ffcd9143bf0(0x7ffcd9143be8)
0x7ffcd9143bf4(0x7ffcd9143bec)
~0x7ffcd9143bf4
~0x7ffcd9143bf0
~0x7ffcd9143bec
~0x7ffcd9143be8
~0x7ffcd9143be4
打印结果分为 4 段。其中,第 2 段为 b = a
的执行结果;第 3 段为 c = b = a
的执行结果。
- 分析
b = a
的执行过程:
我们还是转化成成员函数调用的方式以便于理解:
b.operator=(a);
(1)以参数 a
调用 b
的赋值运算符成员函数,执行 x_ = rhs.x_
更新 b
的成员;
(2)执行 return *this
,等价于 return A(*this)
,即用 b
对象自身复制构造一个匿名对象作为返回值。
(3)离开 b
的赋值运算符成员函数的作用域。
(4)语句 b.operator=(a)
执行结束,此时之前赋值运算符函数返回的匿名对象析构。
综上,一共 1 次复制构造、1 次析构。
- 分析
c = b = a
的执行过程:
等价语句:
c.operator=(b.operator=(a));
(1)执行 b.operator=(a)
,函数返回时复制构造一个对象,该对象是一个临时的匿名对象 Anonymous1。
(2)执行 c.operator=(Anonymous1)
,函数返回时,再次复制构造一个临时对象 Anonymous2。
(3)语句 c.operator=(b.operator=(a))
执行完毕,之前的临时匿名对象依次析构:先析构Anonymous2,再析构 Anonymous1。因为局部变量是放在栈上的,所以析构顺序与变量定义顺序相反。
综上,一共 2 次复制构造、2 次析构。
- 问题:
当我们执行b = a
,由于a
和b
都是已经定义好的对象,不应该有构造和析构过程。但当我们返回值类型的对象时,却凭空多了 1 次构造和 1 次析构的开销!
返回“引用类型”
为了避免返回匿名对象造成的构造和析构开销,我们将返回类型改成引用类型。
测试程序:
#include <bits/stdc++.h> // Linux 系统
using namespace std;
#define __FILENAME__ \
(strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
struct A
{
int x_;
A()
{
cout << this << "()" << endl;
}
A(int x)
{
cout << this << "(" << x << ")" << endl;
}
A(const A &rhs)
{
x_ = rhs.x_;
cout << this << "(" << &rhs << ")" << endl;
}
~A()
{
cout << "~" << this << endl;
}
// 返回引用类型
A& operator=(const A &rhs)
{
x_ = rhs.x_; // 改变成员变量
// return A(*this);
return *this;
}
};
int main()
{
A a(1);
A b(2);
A c(3);
getchar();
b = a; // 等价于 b.operator=(a);
getchar();
c = b = a; // 连续赋值
getchar(); // 暂停,方便观察
}
测试结果:
0x7fffb86d362c(1)
0x7fffb86d3630(2)
0x7fffb86d3634(3)
~0x7fffb86d3634
~0x7fffb86d3630
~0x7fffb86d362c
可见,调用赋值运算符函数的构造和析构过程都消失了。这是符合预期的。
返回“常量引用类型”
那么,我们再提出一个问题:如果返回常量引用类型,可以吗?
答:可以。可以看到,我们之所以返回对象或对象的引用,是为了连续赋值时,将其作为新的参数以再次调用赋值运算符函数,而赋值运算符函数的参数就是一个常量引用,即使传入一个引用(也就是返回引用类型),也和传入常量引用(也就是返回常量引用)没有什么区别。
测试程序:
#include <bits/stdc++.h> // Linux 系统
using namespace std;
#define __FILENAME__ \
(strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
struct A
{
int x_;
A()
{
cout << this << "()" << endl;
}
A(int x)
{
cout << this << "(" << x << ")" << endl;
}
A(const A &rhs)
{
x_ = rhs.x_;
cout << this << "(" << &rhs << ")" << endl;
}
~A()
{
cout << "~" << this << endl;
}
// 返回常量引用
const A& operator=(const A &rhs)
{
x_ = rhs.x_; // 改变成员变量
// return A(*this);
return *this;
}
};
int main()
{
A a(1);
A b(2);
A c(3);
getchar();
b = a; // 等价于 b.operator=(a);
getchar();
c = b = a; // 连续赋值
getchar(); // 暂停,方便观察
}
打印结果:
0x7ffdda7c80ac(1)
0x7ffdda7c80b0(2)
0x7ffdda7c80b4(3)
~0x7ffdda7c80b4
~0x7ffdda7c80b0
~0x7ffdda7c80ac
这和返回引用类型没有什么不同。
但是,以下调用方式就会报错:
// 由运算符优先级,括号不加也可以。
A& ra = (b = a); // 报错:(b = a) 返回一个 const 引用,显然不能将其绑定到引用类型。
const A& ra1 = (b = a); // 正确
但是,一般理解上,b = a
应该返回的是还是 b
,我们将 b
绑定到一个引用上,应该是可行的。所以,为了适应一般性理解,我们重载赋值运算符时一般返回的是引用,而不是常量引用。其实,这也同时解释了为什么我们总是直接返回引用,而不是一个新的对象的原因,因为我们会认为 b = a
返回的应该是 b
本身。