C++那些细节--中operator=相关问题

一.简介

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;
//相当于:
a = (b = c);
即所谓的连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参。

注意,这种操作在赋值操作中都成立。
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 : 定义控制台应用程序的入口点。
//

#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;
}
不过,这里我的 程序会不停的调用swap函数,直到stack overflow...查了半天也没差出来,以后来填坑。


更方便的办法:
CopyTest& operator= (CopyTest& e)
	{
		std::swap(*this, e);
		cout<<"operator= function is called!"<<endl;
		return *this;
	}
直接使用函数的实参,来作为swap的参数,但是注意要把const去掉。


简单总结一下:
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
请按任意键继续. . .

这个真的很容易忽略,我们很有可能只记住了子类要拷贝的东东,而忘记了父类。


十.关于拷贝构造函数和operator=函数中相同的部分怎么处理

看到拷贝构造函数和operator=函数中有很多重复的东东,确实有种强迫症想要使一个函数调用另一个函数,但是这样做并不妥当,两者没什么太大的关系,一个是构造一个不存在的对象,另一个是给已经存在的对象赋值。要说两者的相同之处只是有一部分赋值的代码相同,我们如果确实想要优化就把这一部分代码提取出来,放到一个Init函数中,供两者调用即可。














你可能感兴趣的:(C++,operator,复制构造函数,拷贝构造函数,深拷贝浅拷贝)