一.简介
operator= 运算符重载,就类似正常我们为变量赋值(如a = b)时的情况,但是我们自己写的类,内部肯定有各种字段,赋值当然不会就像a=b那么简单啦。
如果我们自己写重载操作符=,编译器也会为我们生成一个,但是这个函数功能很弱,只能实现浅赋值。
// C++Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class CopyTest
{
private:
string name;
int id;
public:
CopyTest(int i, string n):id(i),name(n)
{
//cout<<"Construct!"<<endl;
}
~CopyTest()
{
//cout<<"No"<<id<<" "<<"Destruct!"<<endl;
}
void Display()
{
cout<<name<<" "<<id<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CopyTest test1(1, "hehe");
CopyTest test2(2, "haha");
cout<<"Before operator = test2 is: ";
test2.Display();
test2 = test1;
cout<<"After operator = test2 is ";
test1.Display();
system("pause");
return 0;
}
结果:
Before operator = test2 is: haha 2
After operator = test2 is hehe 1
请按任意键继续. . .
可见,虽然我们并没有写那个operator=的函数,但是我们仍然实现了对象的赋值。
二.自己DIY一个operator=函数
// C++Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class CopyTest
{
private:
string name;
int id;
public:
CopyTest(int i, string n):id(i),name(n)
{
//cout<<"Construct!"<<endl;
}
~CopyTest()
{
//cout<<"No"<<id<<" "<<"Destruct!"<<endl;
}
CopyTest& operator= (const CopyTest& e)
{
name = e.name;
id = e.id;
cout<<"operator= function is called!"<<endl;
return *this;
}
void Display()
{
cout<<name<<" "<<id<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CopyTest test1(1, "hehe");
CopyTest test2(2, "haha");
cout<<"Before operator = test2 is: ";
test2.Display();
test2 = test1;
cout<<"After operator = test2 is ";
test1.Display();
system("pause");
return 0;
}
结果: Before operator = test2 is: haha 2
operator= function is called!
After operator = test2 is hehe 1
请按任意键继续. . .
这里我们就可以看到,系统调用了我们所写的operator=函数,完成了与系统为我们添加的函数相同的功能。
三.operator=返回一个本对象的引用
这里有个细节,要注意一下,同时这也是《Effictive C++》中的重要一条:
关于operator=函数的返回值,我们的返回值类型是对象本身的引用,return时使用的是this指针所指的位置的内容(即*this的值),为什么要这样呢?
对于=,我们可能会这样使用:
a = b = c;即所谓的连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参。
//相当于:
a = (b = c);
注意,这种操作在赋值操作中都成立。
CopyTest& operator= (const CopyTest& e)
{...
return *this;
}
</pre><pre name="code" class="cpp">//在+=,-=,*=,/=等情况也都成立
当然,这不是强制的,只是一个协议。不遵守程序并无问题,但是这条协议被所有内置数据类型,string,vector等等C++几乎最常用的类型所遵守,我们还是随大流吧...
四.深拷贝浅拷贝
与拷贝构造函数中相同,operator=重载中也含有深拷贝与浅拷贝的概念。浅拷贝即只是字面上的拷贝,不涉及指针等动态空间的拷贝,而深拷贝则是会处理动态内存空间的相关信息。关于浅拷贝和深拷贝,在拷贝构造函数中有更详细的介绍:http://blog.csdn.net/puppet_master/article/details/46955965,如果我们不写operator=的话,编译器会自动为我们生成一个,但是这个自动生成的只能实现浅拷贝,遇到动态内存相关的肯定会出错,所以我们还是自己写一个比较好。// C++Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class CopyTest
{
private:
string name;
int id;
int* pointer;
public:
CopyTest(int i, string n, int* p):id(i),name(n), pointer(p)
{
//cout<<"Construct!"<<endl;
}
~CopyTest()
{
//cout<<"No"<<id<<" "<<"Destruct!"<<endl;
}
CopyTest& operator= (const CopyTest& e)
{
name = e.name;
id = e.id;
delete pointer;
pointer = new int(*e.pointer);
cout<<"operator= function is called!"<<endl;
return *this;
}
void Display()
{
cout<<"name: "<<name<<" "<<"id: "<<id<<" pointer: "<<*pointer<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CopyTest test1(1, "hehe", new int(1));
CopyTest test2(2, "haha", new int (2));
cout<<"Before operator = test2 is: ";
test2.Display();
test2 = test1;
cout<<"After operator = test2 is ";
test1.Display();
system("pause");
return 0;
}
结果: Before operator = test2 is: name: haha id: 2 pointer: 2
operator= function is called!
After operator = test2 is name: hehe id: 1 pointer: 1
请按任意键继续. . .
上面的就是一个最简单的深拷贝,普通的变量还是正常赋值,但是遇到指针时,先把原来指针所指向的内容delete掉,释放内存,然后重新申请一块内存,并将这块内存的值用=右边的指针所指向的内容赋值,用本对象中的指针指向。
五.防止对象自我赋值
上面的例子虽然很好的实现了深拷贝,但是有一个问题,如果我们用一个普通的变量自己给自己赋值,正常是没有问题的,但是深拷贝涉及到指针,和内存释放的问题,试想一下,如果我们自己给自己赋值,先delete掉了pointer所指向的内容,然后我们又new出了一块内存,要给他赋值的时候,发现这个pointer已经被释放了,所以肯定会出问题... 例如,上面的程序我们这样赋值:test2 = test2;那么结果:
Before operator = test2 is: name: haha id: 2 pointer: 2
operator= function is called!
After operator = test2 is name: haha id: 2 pointer: -17891602
请按任意键继续. . .
pointer的值变成了一个很奇怪的数。
也许有人会问,我们不会傻到自己给自己赋值吧? 还真不好说,上面的情况比较极端,如果我们用一个数组循环的时候,说不好i,j重合了,那么就有可能发生这种情况。或者代码比较复杂时,很有可能就会出现自我赋值的情况。 既然我们知道有这个隐患,那么就要消除这个隐患,其实这个很简单,我们只需要在赋值之前判断一下,这两个对象是不是同一个对象即可,如果是同一个对象,那么就直接返回,不进行操作。
// C++Test.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class CopyTest
{
private:
string name;
int id;
int* pointer;
public:
CopyTest(int i, string n, int* p):id(i),name(n), pointer(p)
{
//cout<<"Construct!"<<endl;
}
~CopyTest()
{
//cout<<"No"<<id<<" "<<"Destruct!"<<endl;
}
CopyTest& operator= (const CopyTest& e)
{
if (this == &e)
return *this;
name = e.name;
id = e.id;
delete pointer;
pointer = new int(*e.pointer);
cout<<"operator= function is called!"<<endl;
return *this;
}
void Display()
{
cout<<"name: "<<name<<" "<<"id: "<<id<<" pointer: "<<*pointer<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CopyTest test1(1, "hehe", new int(1));
CopyTest test2(2, "haha", new int (2));
cout<<"Before operator = test2 is: ";
test2.Display();
test2 = test2;
cout<<"After operator = test2 is ";
test2.Display();
system("pause");
return 0;
}
结果: Before operator = test2 is: name: haha id: 2 pointer: 2
After operator = test2 is name: haha id: 2 pointer: 2
请按任意键继续. . .
这样,我们就防止了自我赋值时发生的意外情况。这是《Effictive C++》中的一条。
六.防止拷贝过程中内存申请失败导致原来内容被破坏
解决了上面自我赋值的隐患,我们还有一个隐患,如果赋值过程中new失败了,内存没有申请成功,那么原来的内容已经被删除了,就会出现我们不想看到的情况。要解决这种情况只需要简单的加几句话:CopyTest& operator= (const CopyTest& e)
{
if (this == &e)
return *this;
name = e.name;
id = e.id;
//先用一个临时指针变量存储
int* tempPointer = new int (*e.pointer);
//如果上一步执行成功,才删除原有内容
delete pointer;
//赋值
pointer = tempPointer;
cout<<"operator= function is called!"<<endl;
return *this;
}
结果: Before operator = test2 is: name: haha id: 2 pointer: 2
operator= function is called!
After operator = test2 is name: hehe id: 1 pointer: 1
请按任意键继续. . .
七.去除operator= 和拷贝构造函数中的冗余代码
通过c++的一个函数,swap,我们可以实现值的交换// C++Test.cpp : 定义控制台应用程序的入口点。不过,这里我的程序会不停的调用swap函数,直到stack overflow...查了半天也没差出来,以后来填坑。
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class CopyTest
{
private:
string name;
int id;
int* pointer;
public:
CopyTest(int i, string n, int* p):id(i),name(n), pointer(p)
{
//cout<<"Construct!"<<endl;
}
~CopyTest()
{
//cout<<"No"<<id<<" "<<"Destruct!"<<endl;
}
//拷贝构造函数
CopyTest(const CopyTest& e)
{
name = e.name;
id = e.id;
pointer = new int ();
*pointer = *e.pointer;
cout<<"Copy Construct is called!"<<endl;
}
CopyTest& operator= (const CopyTest& e)
{
CopyTest temp(e);
std::swap(*this, temp);
cout<<"operator= function is called!"<<endl;
return *this;
}
void Display()
{
cout<<"name: "<<name<<" "<<"id: "<<id<<" pointer: "<<*pointer<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CopyTest test1(1, "hehe", new int(1));
CopyTest test2(2, "haha", new int (2));
cout<<"Before operator = test2 is: ";
test2.Display();
test2 = test1;
cout<<"After operator = test2 is ";
test2.Display();
system("pause");
return 0;
}
更方便的办法:
CopyTest& operator= (CopyTest& e)直接使用函数的实参,来作为swap的参数,但是注意要把const去掉。
{
std::swap(*this, e);
cout<<"operator= function is called!"<<endl;
return *this;
}
简单总结一下: 1.确保当对象自我赋值时operator=有良好的行为,其中技术包括比较“来源对象”和“目标对象”的地址,改变顺序达到防止内存分配失败而使原内容被删除,以及copy-and-swap。 2.确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为依然正确。
八.复制对象时勿忘其每一个成分(拷贝构造函数和operator=都要注意)
// C++Test.cpp : 定义控制台应用程序的入口点。结果:
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class CopyTest
{
private:
string name;
int id;
public:
CopyTest(int i, string n):id(i),name(n)
{
//cout<<"Construct!"<<endl;
}
~CopyTest()
{
//cout<<"No"<<id<<" "<<"Destruct!"<<endl;
}
//拷贝构造函数
CopyTest(const CopyTest& e)
{
name = e.name;
id = e.id;
cout<<"Copy Construct is called!"<<endl;
}
CopyTest& operator= (CopyTest& e)
{
if (this == &e)
return *this;
name = e.name;
//id = e.id;如果我们忘记写这一句,编译器并不会报错
cout<<"operator= function is called!"<<endl;
return *this;
}
void Display()
{
cout<<"name: "<<name<<" "<<"id: "<<id<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CopyTest test1(1, "hehe");
CopyTest test2(2, "haha");
cout<<"Before operator = test2 is: ";
test2.Display();
test2 = test1;
cout<<"After operator = test2 is ";
test2.Display();
system("pause");
return 0;
}
Before operator = test2 is: name: haha id: 2
operator= function is called!
After operator = test2 is name: hehe id: 2
请按任意键继续. . .
可见,这里,我们忘记写了一句id的拷贝,于是编译器就老老实实的按照我们的方法执行了。“叫你不用我默认的方法,这下你出错了我也不告诉你...” 这是很难发现的一个错误,所以我们在复制值时,一定要记住所有的变量都要写全,不要漏写。
另外一种情况更加危险,更不容易被我们发现,一定要注意!!! 当我们的这个类又派生出其他的类时,拷贝时一定要把基类的内容也一并拷贝过去!!!
九.派生类中的拷贝要考虑基类部分的拷贝(拷贝构造函数和operator=都要注意)
// C++Test.cpp : 定义控制台应用程序的入口点。结果:
//
#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;
class CopyTest
{
private:
string name;
int id;
public:
CopyTest(int i, string n):id(i),name(n)
{
//cout<<"Construct!"<<endl;
}
~CopyTest()
{
//cout<<"No"<<id<<" "<<"Destruct!"<<endl;
}
//拷贝构造函数
CopyTest(const CopyTest& e)
{
name = e.name;
id = e.id;
cout<<"CopyTest CopyConstructor is called!"<<endl;
}
CopyTest& operator= (const CopyTest& e)
{
if (this == &e)
return *this;
name = e.name;
id = e.id;
cout<<"CopyTest operator= function is called!"<<endl;
return *this;
}
virtual void Display()
{
cout<<"name: "<<name<<" "<<"id: "<<id<<endl;
}
};
class CopyTestChild : public CopyTest
{
private:
int childId;
public:
CopyTestChild(int i, string n, int ci): CopyTest(i, n), childId(ci)
{
//cout<<"CopyTestCilid is Constructed!"<<endl;
}
~CopyTestChild()
{
}
//子类拷贝构造函数调用基类拷贝构造函数
CopyTestChild(const CopyTestChild& e) : CopyTest(e)
{
childId = e.childId;
cout<<"CopyTestChild CopyConstructor is called!"<<endl;
}
CopyTestChild& operator= (const CopyTestChild& e)
{
if (this == &e)
return *this;
//调用基类operator=函数
CopyTest::operator=(e);
childId = e.childId;
cout<<"CopyTestChild operator= function is called!"<<endl;
return *this;
}
virtual void Display()
{
CopyTest::Display();
cout<<"ChildId is "<<childId<<endl;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CopyTestChild test1(1, "hehe", 1);
CopyTestChild test2(2, "haha", 2);
cout<<"Before operator = test2 is: ";
test2.Display();
test2 = test1;
cout<<"After operator = test2 is ";
test2.Display();
system("pause");
return 0;
}
Before operator = test2 is: name: haha id: 2
ChildId is 2
CopyTest operator= function is called!
CopyTestChild operator= function is called!
After operator = test2 is name: hehe id: 1
ChildId is 1
请按任意键继续. . .
拷贝构造函数的情况:
int _tmain(int argc, _TCHAR* argv[])
{
CopyTestChild test1(1, "hehe", 1);
CopyTestChild test2(test1);
test2.Display();
system("pause");
return 0;
}
结果: CopyTest CopyConstructor is called!
CopyTestChild CopyConstructor is called!
name: hehe id: 1
ChildId is 1
请按任意键继续. . .
这个真的很容易忽略,我们很有可能只记住了子类要拷贝的东东,而忘记了父类。