一文说尽C++赋值运算符重载函数(operator=)
写在前面:
C++的赋值运算符重载函数(operator=),更“正统”的叫法是“拷贝赋值运算符”(Copy Assignment Operator),关于它,网络以及各种教材上都有很多介绍,但可惜的是,内容大多雷同且不全面。针对这一局面,在下在整合各种资源及融入个人理解的基础上,整理出一篇较为全面/详尽的文章,以飨读者。请读者注意,为了行文简洁,下文中,在不引发混淆的情况下,一律用“函数”指代赋值运算符重载函数。
正文:
Ⅰ.举例
#include<iostream> #include<string> using namespace std; class MyStr { private: char *name; int id; public: MyStr():id(0),name(NULL) {} MyStr(int _id, char *_name) //constructor { cout << "constructor" << endl; id = _id; name = new char[strlen(_name) + 1]; strcpy_s(name, strlen(_name) + 1, _name); } MyStr(const MyStr& str) { cout << "copy constructor" << endl; id = str.id; name = new char[strlen(str.name) + 1]; strcpy_s(name, strlen(str.name) + 1, str.name); } MyStr& operator =(const MyStr& str)//赋值运算符 { cout << "operator =" << endl; if (this != &str) { if (name != NULL) delete[] name; this->id = str.id; int len = strlen(str.name); name = new char[len + 1]; strcpy_s(name, strlen(str.name) + 1, str.name); } return *this; } ~MyStr() { delete[] name; } }; int main() { MyStr str1(1, "hhxx"); cout << "====================" << endl; MyStr str2; str2 = str1; cout << "====================" << endl; MyStr str3 = str2; return 0; }
结果:
Ⅱ.参数
一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用(如上面例1),加const是因为:
①我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。
②加上const,对于const的和非const的实参,函数都能接受;如果不加,就只能接受非const的实参。
用引用是因为:
这样可以避免在函数调用时对实参的一次拷贝,提高了效率。
注意:
1. 如果函数是按值返回的(尽管我们不推荐这样做),在这种情况下,为了实现 a = b = c; 这样的连续赋值,把参数类型声明为 const Type& (这里 Type 表示函数所在类的类型)是唯二的选择,另一个选择是声明为 Type ,即按值传递。原因和例子会在 Ⅻ.再论参数和返回值 一节给出。
2. 上面的规定都不是强制的,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象,正如后面例2中的那样。
Ⅲ.返回值
一般地,返回值是被赋值者的引用,即*this(如上面例1),原因是
①这样在函数返回时避免一次拷贝,提高了效率。
②更重要的,这样可以实现连续赋值,即类似a=b=c这样。如果不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,由于返回的是值类型,所以要对return后边的“东西”进行一次拷贝,得到一个未命名的副本(有些资料上称之为“匿名对象”),然后将这个副本返回,而这个副本是右值,所以,执行a=b后,得到的是一个右值,再执行=c就会出错。更正:各位读者,对不起!这里的表述是一个严重的错误,现已更正,请移步 Ⅻ.再论参数和返回值 一节。
注意:
这也不是强制的,我们可以将函数返回值声明为void,然后什么也不返回,只不过这样就不能够连续赋值了。
Ⅳ.调用时机
当为一个类对象赋值(注意:可以用本类对象为其赋值(如上面例1),也可以用其它类型(如内置类型)的值为其赋值,关于这一点,见后面的例2)时,会由该对象调用该类的赋值运算符重载函数。
如上边代码中
str2 = str1;
一句,用str1为str2赋值,会由str2调用MyStr类的赋值运算符重载函数。
需要注意的是,
MyStr str2;
str2 = str1;
和
MyStr str3 = str2;
在调用函数上是有区别的。正如我们在上面结果中看到的那样。
前者MyStr str2;一句是str2的声明加定义,调用无参构造函数,所以str2 = str1;一句是在str2已经存在的情况下,用str1来为str2赋值,调用的是拷贝赋值运算符重载函数;而后者,是用str2来初始化str3,调用的是拷贝构造函数。
Ⅴ.提供默认赋值运算符重载函数的时机
当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。可见,所谓默认,就是“以本类或本类的引用为参数”的意思。
#include<iostream> #include<string> using namespace std; class Data { private: int data; public: Data() {}; Data(int _data) :data(_data) { cout << "constructor" << endl; } Data& operator=(const int _data) { cout << "operator=(int _data)" << endl; data = _data; return *this; } }; int main() { Data data1(1); Data data2,data3; cout << "=====================" << endl; data2 = 1; cout << "=====================" << endl; data3 = data2; return 0; }
结果:
上面的例子中,我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1;一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2;一句将不会编译通过,但我们看到事实并非如此。所以,这个例子有力地证明了我们的结论。
Ⅵ.构造函数还是赋值运算符重载函数
如果我们将上面例子中的赋值运算符重载函数注释掉,main函数中的代码依然可以编译通过。只不过结论变成了
可见,当用一个非类A的值(如上面的int型值)为类A的对象赋值时
①如果匹配的构造函数和赋值运算符重载函数同时存在(如例2),会调用赋值运算符重载函数。
②如果只有匹配的构造函数存在,就会调用这个构造函数。
Ⅶ.显式提供赋值运算符重载函数的时机
①用非类A类型的值为类A的对象赋值时(当然,从Ⅵ中可以看出,这种情况下我们可以不提供相应的赋值运算符重载函数而只提供相应的构造函数来完成任务)。
②当用类A类型的值为类A的对象赋值且类A的成员变量中含有指针时,为避免浅拷贝(关于浅拷贝和深拷贝,下面会讲到),必须显式提供赋值运算符重载函数(如例1)。
Ⅷ.浅拷贝和深拷贝
拷贝构造函数和赋值运算符重载函数都会涉及到这个问题。
所谓浅拷贝,就是说编译器提供的默认的拷贝构造函数和赋值运算符重载函数,仅仅是将对象a中各个数据成员的值拷贝给对象b中对应的数据成员(这里假设a、b为同一个类的两个对象,且用a拷贝出b或用a来给b赋值),而不做其它任何事。
假设我们将例1中显式提供的拷贝构造函数注释掉,然后同样执行MyStr str3 = str2;语句,此时调用默认的拷贝构造函数,它只是将str2的id值和nane值拷贝到str3,这样,str2和str3中的name值是相同的,即它们指向内存中的同一区域(在例1中,是字符串”hhxx”)。如下图
这样,会有两个致命的错误
①当我们通过str2修改它的name时,str3的name也会被修改!
②当执行str2和str3的析构函数时,会导致同一内存区域释放两次,程序崩溃!
这是万万不可行的,所以我们必须通过显式提供拷贝构造函数以避免这样的问题。就像我们在例1中做的那样,先判断被拷贝者的name是否为空,若否,delete[] name(后面会解释为什么要这么做),然后,为name重新申请空间,再将拷贝者name中的数据拷贝到被拷贝者的name中。执行后,如图
这样,str2.name和str3.name各自独立,避免了上面两个致命错误。
我们是以拷贝构造函数为例说明的,赋值运算符重载函数也是同样的道理。
Ⅸ.赋值运算符重载函数只能是类的非静态的成员函数
C++规定,赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数。关于原因,有人说,赋值运算符重载函数往往要返回*this,而无论是静态成员函数还是友元函数都没有this指针。这乍看起来很有道理,但仔细一想,我们完全可以写出这样的代码
static friend MyStr& operator=(const MyStr str1,const MyStr str2) { …… return str1; }
可见,这种说法并不能揭露C++这么规定的原因。
其实,之所以不是静态成员函数,是因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的。
在前面的讲述中我们说过,当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。现在,假设C++允许将赋值运算符重载函数定义为友元函数并且我们也确实这么做了,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,所以,此时编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。此时,我们再执行类似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?
为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。
Ⅹ. 赋值运算符重载函数不能被继承
见下面的例3
#include<iostream> #include<string> using namespace std; class A { public: int X; A() {} A& operator =(const int x) { X = x; return *this; } }; class B :public A { public: B(void) :A() {} };
int main() { A a; B b; a = 45; //b = 67; (A)b = 67; return 0; }
注释掉的一句无法编译通过。报错提示:没有与这些操作数匹配的”=”运算符。对于b = 67;一句,首先,没有可供调用的构造函数(前面说过,在没有匹配的赋值运算符重载函数时,类似于该句的代码可以调用匹配的构造函数),此时,代码不能编译通过,说明父类的operator =函数并没有被子类继承。
为什么赋值运算符重载函数不能被继承呢?
因为相较于基类,派生类往往要添加一些自己的数据成员和成员函数,如果允许派生类继承基类的赋值运算符重载函数,那么,在派生类不提供自己的赋值运算符重载函数时,就只能调用基类的,但基类版本只能处理基类的数据成员,在这种情况下,派生类自己的数据成员怎么办?
所以,C++规定,赋值运算符重载函数不能被继承。
上面代码中, (A)b = 67; 一句可以编译通过,原因是我们将B类对象b强制转换成了A类对象。
Ⅺ.赋值运算符重载函数要避免自赋值
对于赋值运算符重载函数,我们要避免自赋值情况(即自己给自己赋值)的发生,一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象(正如例1中的if (this != &str)一句)。
为什么要避免自赋值呢?
①为了效率。显然,自己给自己赋值完全是毫无意义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。
②如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?代码可以参考例1。
所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。
Ⅻ.再论参数和返回值
不可否认,在下当年写下这篇文章时,对“Copy Assignment Operator的参数和返回值类型与连续赋值的关系”的理解存在严重错误!对于文章对大家的误导,在下深表歉意和自责,这里向大家诚挚道歉:各位读者,之前文章存在知识性错误,对不起!请允许我改正它!这里也特别感谢留言指正的读者:谢谢各位的批评和指正,是您各位的留言让我重视并重新审视了这个问题,感谢各位!
本节代码编译环境:Win10,MinGW-W64 x86_64-posix-seh 12.0.1, g++ main.cpp -o main -std=c++20 -O3 。其它环境下应该也能得到同样结果,如您在复现本节代码时发现了不一样的结果,烦请您不吝告知。
本节内容参考了资料[1],读者可以直接前往。本节在拾人牙慧的基础上做了更进一步地对比和分析,分以下部分讨论:
预备知识
为理解本节内容,读者需了解以下知识点:
- a = b = c; 的赋值是从右往左的,等价于 a = (b = c); 。在下之前之所以理解出错,很大原因就是没有弄清这个基本知识点。
- const的左值引用(即 const Type& )可以绑定到右值。这一点,请读者通过介绍“C++ 左值引用 右值引用”相关主题的资料自行学习。
- a = b; 相当于 a.operator=(b); , a = b = c; 相当于 a.operator=(b.operator=(c)); ,甚至,您可以直接在代码里这么写,就如您即将看到的。这里,您可以把 operator= 理解为函数名,尽管它看起来有点奇怪,但它和 add 这样的函数名是一样的。
- 显然,对于 a = b; 这样没有连续赋值的,返回值根本没有被用到;对于 a = b = c; 或 a.operator=(b.operator=(c)); , b = c 或 b.operator=(c) 的返回值会作为下次调用(即给 a 赋值时调用的Copy Assignment Operator)的实参。
参数和返回值类型没有特殊限定
就在下所知,编译器没有对Copy Assignment Operator的参数类型及返回值类型做任何强制规定,您可以随心所欲地写,原则只有一个:您编写的Copy Assignment Operator工作起来符合您的预期。比如,下面这个“离经叛道”的Copy Assignment Operator可以正常工作,尽管它并不高效。
1 #include <iostream> 2 3 using std::cout; 4 5 class CKiloMeter 6 { 7 public: 8 CKiloMeter(double _v) : m_value(_v) {} 9 ~CKiloMeter() {} 10 double getValue() { return m_value; } 11 12 private: 13 double m_value; 14 }; 15 16 class CMeter 17 { 18 public: 19 CMeter(double _v) : m_value(_v) {} 20 ~CMeter() {} 21 // copy assignment operator, odd but workable 22 void operator=(CKiloMeter km) 23 { 24 this->m_value += km.getValue() * 1000; 25 } 26 double getValue() { return m_value; } 27 28 private: 29 double m_value; 30 }; 31 32 int main() 33 { 34 CMeter m(10.0); 35 CKiloMeter km(1.0); 36 m = km; 37 cout << m.getValue() << '\n'; 38 return 0; 39 }
支持连续赋值的多种情况
情况1:[const] Type operator=([const] Type rhs)
这种情况是pass by value & return by value,其中,[const]表示const是可选的,加不加对结果没有影响,下同。请看如下代码:
1 #include <iostream> 2 3 using std::cout; 4 5 class CMeter 6 { 7 public: 8 CMeter(double _v) : m_value(_v) {} 9 ~CMeter() {} 10 // copy constructor 11 CMeter(const CMeter &m) 12 { 13 cout << "copy constructor called.\n"; 14 this->m_value = m.m_value; 15 } 16 // rhs: right-hand side 17 // pass by value & return by value 18 CMeter operator=(CMeter rhs) 19 { 20 this->m_value = rhs.getValue(); 21 return *this; 22 } 23 double getValue() const { return m_value; } 24 25 private: 26 double m_value; 27 }; 28 29 int main() 30 { 31 CMeter m1(1.0), m2(2.0); 32 // m1.operator=(m2); 33 m1 = m2; 34 cout << "m1: " << m1.getValue() << '\n'; 35 cout << "m2: " << m2.getValue() << '\n'; 36 37 CMeter m3(3.0), m4(4.0), m5(5.0); 38 // m3.operator=(m4.operator=(m5)); 39 m3 = m4 = m5; 40 cout << "m3: " << m3.getValue() << '\n'; 41 cout << "m4: " << m4.getValue() << '\n'; 42 cout << "m5: " << m5.getValue() << '\n'; 43 44 return 0; 45 }
说明:
- 将18行的 CMeter operator=(CMeter rhs) 换成 CMeter operator=(const CMeter rhs) 或 const CMeter operator=(CMeter rhs) 或 const CMeter operator=(const CMeter rhs) ,结果不变。
- 将32行的注释去掉,转而将33行注释掉,结果不变;38、39行同理。
结果如下:
结果分析如下:
copy constructor called. // 执行m1.operator=(m2)时pass by value传参,发生一次拷贝 copy constructor called. // m1.operator=(m2)返回时,return by value,发生一次拷贝 m1: 2 m2: 2 copy constructor called. // 执行m3.operator=(m4.operator=(m5))中的m4.operator=(m5)时pass by value传参,发生一次拷贝 copy constructor called. // m4.operator=(m5)返回时,return by value,发生一次拷贝 copy constructor called. // m3.operator=()调用返回时,return by value,发生一次拷贝 m3: 5 m4: 5 m5: 5
这里有以下情况需要说明:
- 对于 m3 = m4 = m5; 或 m3.operator=(m4.operator=(m5)); , m4 = m5 或 m4.operator=(m5) 返回的临时对象直接作为了的 m3.operator=([const] CMeter rhs) 的参数,不存在“从临时对象再拷贝出一份临时对象然后传给 m3.operator=([const] CMeter rhs) ”的情况。更详细的情况,可能就需要反汇编来看了,但本文暂时就不深入到这一步了。
- 对于 const CMeter operator=(CMeter rhs) ,可能会有读者疑惑: m4 = m5 或 m4.operator=(m5) 返回的const临时对象直接传给了要求non-const参数的 m3.operator=(CMeter rhs) ,这不就是把const的东西变成了non-const的东西了吗,怎么可以?原因是: m4 = m5 或 m4.operator=(m5) 返回的是 m4 的拷贝,而且还是一份临时拷贝,这个临时对象是否被修改,都不会影响 m4 的值,也不会影响程序的正确性,因此将它传给 a.operator=(CMeter rhs) 是可以的。另外,从语法上讲,将一个const实参按值传递给一个non-const形参也是被允许的,读者可以自行编码验证。
情况2:[const] Type &operator=([const] Type rhs)
这种情况是pass by value & return by reference。本小节测试代码为:将情况1中代码第18行改为 CMeter &operator=(CMeter rhs) 或 CMeter &operator=(const CMeter rhs) 或 const CMeter &operator=(CMeter rhs) 或 const CMeter &operator=(const CMeter rhs) ,其余均保持不变。
结果如下:
结果分析如下:
这里不详细讨论“为什么形参和返回值加不加const都不影响结果”这一问题,只提一点:将 const或non-const引用 按值传递给 const或non-const形参,都是可以的。读者可自行编码验证并仔细逐一分析上面4种情况。下面分析构造函数调用情况。
copy constructor called. // 执行m1.operator=(m2)时pass by value传参,发生一次拷贝;由于return by reference,函数返回时不发生拷贝 m1: 2 m2: 2 copy constructor called. // 执行m3.operator=(m4.operator=(m5))中的m4.operator=(m5)时pass by value传参,发生一次拷贝 copy constructor called. // m4.operator=(m5)返回时,return by reference,不发生拷贝;但返回的m4的引用需要按值传递给m3.operator=(CMeter rhs),因此在传参时发生一次拷贝;m3的调用返回时,由于是return by reference,故不发生拷贝 m3: 5 m4: 5 m5: 5
这里多说一句,对于 a = b =c; ,在情况1中,共发生了3次对象拷贝,其中,由于传参发生了一次拷贝,由于函数返回发生了两次拷贝。相比于情况1,情况2是return by reference,所以那不应该是发生3-2=1次拷贝吗?为什么实际上是两次呢?因为 m4.operator=(m5) 返回的是 m4 的引用,而该引用需要按值传递给 m3.operator=(CMeter rhs) 的形参,因此这里需要一次拷贝。
情况3:[const] Type operator=(const Type &rhs)
这种情况是pass by reference & return by value。本小节的测试代码为:将情况1中代码第18行改为 CMeter operator=(const CMeter &rhs) 或 const CMeter operator=(const CMeter &rhs) ,其余均保持不变。
结果如下:
结果分析如下:
copy constructor called. // 执行m1.operator=(m2)时pass by reference传参,不发生拷贝;但由于return by value,函数返回时发生一次拷贝 m1: 2 m2: 2 copy constructor called. // 执行m3.operator=(m4.operator=(m5))中的m4.operator=(m5)时pass by reference传参,不发生拷贝;但由于return by value,函数返回时发生一次拷贝 copy constructor called. // m4.operator=(m5)返回(return by value)的临时对象作为实参传递给m3.operator=(const CMeter &rhs),因为是pass by reference传参,不发生拷贝;但m3.operator=(const CMeter &rhs)返回时,由于是return by value,故发生一次拷贝 m3: 5 m4: 5 m5: 5
这里需要特别说明的是,参数中的const是必须的。如果将函数原型写成 CMeter operator=(CMeter &rhs) 或 const CMeter operator=(CMeter &rhs) , m3 = m4 = m5; 或 m3.operator=(m4.operator=(m5)); 这样的连续赋值是编译不过去的,g++下会报类似 cannot bind non-const lvalue reference of type 'CMeter&' to an rvalue of type 'CMeter' 这样的错误。
原因是:由于return by value, m4 = m5 或 m4.operator=(m5) 返回的是一个匿名的临时对象,即一个右值(rvalue),而 CMeter &rhs 这样的non-const左值引用(non-const lvalue reference)是不能绑定到右值的。但是,正如预备知识中的第2点所示, const CMeter &rhs 这样的const lvalue reference却是可以绑定到右值的。
情况4:Type &operator=(Type &rhs)
这种情况是pass by reference & return by reference。本小节的测试代码为:将情况1中代码第18行改为 CMeter &operator=(CMeter &rhs) 或 CMeter &operator=(const CMeter &rhs) 或 const CMeter &operator=(const CMeter &rhs) ,其余均保持不变。
结果如下:
结果分析如下:因为传参和返回都是by reference的,所以不会发生拷贝。
这里需要特别说明的是,如果将函数原型改成 const CMeter &operator=(CMeter &rhs) , m3 = m4 = m5; 或 m3.operator=(m4.operator=(m5)); 这样的连续赋值是编译不过去的,g++下会报类似 binding reference of type 'CMeter&' to 'const CMeter' discards qualifiers 这样的错误。原因也很简单, m4 = m5 或 m4.operator=(m5) 返回的是const reference,将const reference传递给non-const reference,属于“放宽政策”,肯定是不允许的。
(a = b) = c简单讨论
尽管C++的内置类型都支持这种写法,但很显然,这是一句写了会被扣钱的代码,因此本文不会深入讨论它。为了文章的完整性,本文给出下面这张您几乎从来不会用到的表格,它存在的唯一意义是:如果您完全搞懂了表格中的内容,那说明您对Copy Assignment Operator、函数参数传递、函数返回、const、lvalue、rvalue 等知识点已经掌握得不错了。
本小节测试代码为:在情况1代码的基础上,将38行语句改为 m3.operator=(m4).operator=(m5); ,将39行语句改为 (m3 = m4) = m5; ,同时删除 m1 = m2; 相关的测试代码,然后更换不同原型的Copy Assignment Operator。
能够使 (m3 = m4) = m5; 或 m3.operator=(m4).operator=(m5); (两者结果是一样的)编译通过的函数原型以及对应的结果如下表所示。
情形 | 函数签名 | 结果 | 分析 |
pass by value & return by value |
CMeter operator=(CMeter rhs) // or CMeter operator=(const CMeter rhs) |
① m3.operator=(m4) 传参和返回各发生一次拷贝, m3 的值被修改为 m4 的值; ② m3.operator=(m4) 返回的是函数调用者,即 m3 的副本(临时对象),这个临时对象 作为caller调用 operator=(m5) ,传参和返回各发生一次拷贝; ③由于caller(即被赋值者)是 m3 的副本而非 m3 本身,因此 m3 没有被修改为 m5 的值。 |
|
pass by reference & return by value |
CMeter operator=(CMeter &rhs) // or CMeter operator=(const CMeter &rhs) |
① m3.operator=(m4) 返回时发生一次拷贝, m3 的值被修改为 m4 的值; ② m3.operator=(m4) 返回的是函数调用者,即 m3 的副本(临时对象),这个临时对象 作为caller调用 operator=(m5) ,返回时发生一次拷贝; ③由于caller(即被赋值者)是 m3 的副本而非 m3 本身,因此 m3 没有被修改为 m5 的值。 |
|
pass by value & return by reference |
CMeter &operator=(CMeter rhs) // or CMeter &operator=(const CMeter rhs) |
① m3.operator=(m4) 传参时发生一次拷贝, m3 的值被修改为 m4 的值; ② m3.operator=(m4) 返回的是函数调用者,即 m3 的引用,这个引用 作为caller调用 operator=(m5) ,传参时发生一次拷贝; ③由于caller(即被赋值者)是 m3 的引用,因此 m3 最终被修改为 m5 的值。 |
|
pass by reference & return by reference |
CMeter &operator=(CMeter &rhs) // or CMeter &operator=(const CMeter &rhs) |
同上,只不过由于是pass by reference,省去了两次传参时的拷贝。 |
总结:
- 对于 CMeter [&]operator=([const] [&]rhs) (伪代码,[]表示其中的内容可加可不加)这样的函数原型(简言之,就是返回类型不加const就行), (m3 = m4) = m5; 都可以编译通过。这里不详细展开,相信弄懂了本文内容的读者可以自行分析清楚。这里仅举一例:
- 对于CMeter operator=(CMeter &rhs) ,为什么 m3 = m4 = m5; 不能编译通过(见情况3)而 (m3 = m4) = m5; 却可以(见上表第2行)?原因是——对于 m3 = m4 = m5; ,第一次函数调用的实参是 m5 ,返回值是 m4 的一份拷贝;第二次函数调用的实参是前一次调用的返回值,即 m4 的副本,是个rvalue,将其绑定到non-const lvalue reference(即 CMeter &rhs )是不被允许的(这一点前面就说过)。而对于 (m3 = m4) = m5; ,第一次函数调用的实参是 m4 ,返回值是 m3 的副本;第二次函数调用的caller是前一次调用的返回值,即 m3 的副本,实参是 m5 ;两次调用的实参(即 m4 和 m5 )都是lvalue,而将lvalue绑定到non-const lvalue reference是可以的。
- 如果函数return by value, m3 最终被修改为 m4 的值(上表第1、2行);如果函数return by reference, m3 最终被修改为 m5 的值(上表第3、4行)。原因在表格中已经分析过了。
- 如果给函数返回类型加上const,本节代码就不会编译通过(上表中函数原型的返回类型都没有加const)。原因也很简单, (m3 = m4) 的返回值(无论是临时对象还是引用)是下一个赋值的被操作数,也就是下一个Copy Assignment Operator的调用者,即 this 指针指向的对象。因此,如果函数返回值是const的,那 this 指针就是const的,因此在下一个Copy Assignment Operator中, this->m_value = m.m_value; 一句(情况1中第20行代码)就不会编译通过。因此,给返回类型加上const,可以禁止 (m3 = m4) = m5 这样的赋值操作。
有必要返回const引用吗
结论:如果您想要明确禁止 (m3 = m4) = m5; 这样的赋值,就加上const,否则,就不用加,加了也没啥用。
嫌麻烦的读者可以跳过下面的分析。
分两步回答这个问题。
(1)有必要返回引用吗?——有,可以省去函数返回时的拷贝。
(2)有必要返回const引用吗?——多数情况下属实大可不必。
- 对于m1 = m2;,如前所述,返回值根本没有被用到,爱返回啥返回啥。
- 对于 m3 = m4 = m5; ,根据情况2和情况4,返回类型加不加const对结果没有任何影响。(对情况4,返回类型加const,参数类型就必须加const,但不同版本的函数原型对结果没有任何影响。)
- 对于 (m3 = m4) = m5; ,加上const后代码就跑不通了,原因上一小节已经分析过。
综上,可以得出本小节开头的结论。
“标准”写法
所谓“标准”写法,就是在没有特殊需求(如需要禁止 (a = b) = c; )的情况下,通用的、墨守成规的、Code Review时不会被怼的写法。很显然,这个“标准”写法就是
Type &operator=(const Type &rhs)
首先,pass by reference & return by reference省去了对象拷贝的开销;其次,const reference作为参数保证了“赋值给别人的那个对象”不会在函数中被修改;最后, a = b = c; 这样的连续赋值也能够被支持。当然, (a = b) = c; 这样的赋值也是支持的,如果您想要禁止它,就给返回值也加上const吧。
结束语:
至此,本文的所有内容都介绍完了。再次感谢各位读者的批评指正,但由于在下才疏学浅,错误纰漏之处依然在所难免,如果您在阅读过程中发现了本文的任何错误或不足,请您务必指出。您的批评就是在下前进的不竭动力!
参考
[1] 2 Wrong Way to Learn Copy Assignment Operator in C++ With Example