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

时间:2020-12-20 19:55:42

一.简介

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函数中,供两者调用即可。