C++(5)——拷贝构造函数,运算符重载

拷贝构造函数

同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或者拷贝是完全可行的。这个拷贝过程只需要拷贝数据成员,而函数成员(只有一分拷贝),在建立对象时可用同一类的另一个对象来初始化该对象的存储空间,这时所用的构造函数称为拷贝构造函数。

class Object:
{
	int value;
public:
	Object(){}						//缺省构造函数
	Object(int x = 0):value(x){}	//普通构造函数
	~Object(){}						//缺省析构函数
	//拷贝构造函数
	Object(const Object & obj):value(obj.value)
	{
		cout<<"Create Cpoy"<<endl;
	}
};

int main()
{
	Object obj(10);
	Object obj(obja);
}

如果将拷贝构造函数中的引用符号去掉,会出现什么问题?
——死递归,因为调动拷贝构造函数时,拷贝构造中还有一个obj,会反复调动行成死递归。
辨析下列程序一共生成了几个对象?

Object fun(Object obj)//3 objx拷贝构造obj
{
	int val = obj.Getvalue;
	Object obja(val);//4
	return obja;//
}
int main()
{
	Object objx(0);//1
	Object objy(0);//2
	objy = fun(objx);
	return 0;
}

fun函数快结束时,返回obja时,并不是直接将obja传给objy,而是利用一个将亡值(临时量),此时需要调动拷贝构造函数在该临时空间构建第五个对象。
不难看出,上述程序在调用过程中生成了五个对象,对于空间和资源的利用较大,不太理想,那么我们如何修改程序,使得产生最少的对象却又能够到达同样的效果呢?

  • 形参添加引用,减少一次构造函数的调动,为了防止对obj的修改,添加const(此时不需要形参去改变实参)
Object fun(const Object &obj)//3 objx拷贝构造obj
{
	int val = obj.Value() + 10;
	Object obja(val);//4
	return obja;//
}
int main()
{
	Object objx(0);//1
	Object objy(0);//2
	objy = fun(objx);
	return 0;
}

图示为各对象的存储空间和关系分布:C++(5)——拷贝构造函数,运算符重载

问题:为什么obja将亡值对象构建在了主函数的栈帧空间中,而不是fun函数的栈帧空间中?——(构建在调用者空间中)
因为主函数是调用者,其调用了fun,若构建在fun的栈帧空间中,fun函数结束,就无法得到该对象了

若将上述函数修改为以引用的形式返回?会有哪些变化?想一想之前学过的以引用返回的坏处。

Object & fun(const Object &obj)

C++(5)——拷贝构造函数,运算符重载
此时,由于返回的是一个引用,所以不需要中间变量,故直接将obja的地址出传递给Eax寄存器,但是传递之后,fun函数结束,那么其中空间的对象会自动调动析构函数销毁自身,并将所在的栈空间回收系统,此时,主函数通过Eax内的值解引用,并不一定会正确访问到所期望的结果,这取决于这块空间是否在这段时间内受到其他资源的利用和侵扰。
所以,尽量不要使用引用返回,一定要万分谨慎。如果一定要以引用返回,还是那句话,此时该对象的生存期不应受到函数生存期的影响。

总结:以引用返回和不以引用返回的区别:

  • 以引用返回,将亡值对象构建在被调用函数的栈帧空间中,在该对象调用析构函数后,已死亡的对象被访问,主函数的使用会受到影响
  • 不以引用返回,将亡值对象构建在主函数的栈帧空间中,使用过后再析构,较为安全。
  • (这里还衍生出一个问题在VS2019中,变量的地址在不同次的运行时是不一样的,这也就更加保证了程序的安全,这与win10兼容,正因为这一机制有时候并不会干预以引用返回的地址空间,所以在这类编译器中可以正常运行,但实际上,以引用返回的对象已经死亡,即便可行,我们仍然不推荐这种写法,在VC6.0中每次的地址一样,是因为会覆盖之前使用的空间。)

运算符重载

运算符的重载实际是一种特殊的函数重载,必须定义一个函数,并告诉C++编译器,当遇到该重载的运算符时调用此函数。这个函数叫做运算符重载函数,通常为类的成员函数。
定义运算符重载函数的一般格式:

返回值类型 类名::operator重载的运算符(参数表)
	{……}

operator是关键字,它与重载的运算符一起构成函数名。
今天对于该问题的学习,我们以下面的类为例:

class Int
{
	private:
		int value;
	public:
		Int(int x = 0):valie(x){}
		Int(const Int &it):value(it.value){}
		~Int(){}
}

我们先来写一个加法函数:Add

int main()
{
	Int a(10),b(10);
	Int c;
	c = a.Add(b); //c.value = a.value + b.value;
	// c = Add(&a,b);
}

第一种:这种写法创建了3个对象,x(拷贝构造),tmp,临时对象(返回给c)

Int Add(Int x)
//Int Add(Int * const this ,Int x)
{
	int val = this->value + x.value;
	Int tmp(val);
	return tmp;
}

第二种:添加引用,只创建了一个临时对象

Int Add(Int &x)
//Int Add(Int * const this ,Int &x)
{
	int val = this->value + x.value;
	return Int(val);
}

第三种:为避免修改x或this对象的属性值,为了避免这种错误,我们常常将此类方法写为常方法

Int Add(Int &x)
//Int Add(Int * const this ,Int &x)
{
	x.value += this->value;//改了x
	//this->value += x.value;//改了this(b)
	return Int(x.value);
}
Int Add( const Int &x) const
//Int Add( const Int * const this ,Int &x)

改法如下:

Int Add( const Int &x) const
{
	int val = this->value + x.value;
	return Int(val);
}

函数名Add被改为+号是不被允许的,因此我们需要如下的运算符重载函数:

Int operator+(const Int &x) const
//Int operator+(const Int * const this, const Int &x) const
{
	int val = this->value + x.value;
	return Int(val);
}

编译过程:

c = a+ b;
c = a.operator+(b);
c = operator+(&a,b); 

减法,乘法很好改写,只有除法需要处理一下:

Int operator/( const Int &x) const
{
	if(x.value == 0)exit(EXIT_FAILURE);
	int val = this->value / x.value;
	return Int(val);
}

C++中禁止重载4个的运算符:C++(5)——拷贝构造函数,运算符重载
下面进一步完善Add函数,将“对象+对象”扩展到“对象+ 变量”和“变量 + 对象”

1.对象+变量

Int operator+(const int x) const
{
	int val = this->value + x;
	return Int(val);
	//return *this + Int(x);
	//Int(x)将构建一个对象
	//这一句可以将其转化为对象+对象,只是Int(x)没有姓名,调试到此处会转调对象+对象
}

思考:对于内置类型变量x是否需要添加引用?

答:不需要,因为加了引用后,从引用的本质上来看,反而增加了对内存的访问次数。

2.变量+对象

我们常会写成这样:

Int operator+(const int x , const Int &it) 

但由于参数过多,编译不会通过,运算符重载函数不允许参数过多,这里有三个参数。
因此不能将这个函数设置成类的成员函数,而需要将其设计成全局函数
当改为全局函数时,就没了this指针。

Int operator+(const int x,const Int &it) 
{
	return it + x;
	//变量+对象——》对象+变量-》对象+对象
}

下面我们学完了一些基本的函数,其实任何一个类型,在C++中系统默认添加6个缺省函数

class Empty
{
public:
//构造函数
	Empty(){}
//析构函数
	~Empty(){}
//拷贝构造函数
	Empty(const Empty & e){}
//赋值
	Empty & operator=(const Empty &x)
	{
		return *this;
	}
//取地址符(普通对象)
Empty * operator&() {return this;}
//重载取地址符(常对象)
const Empty * operator&() const {return this;}
}

在C11标准中,变成了8个缺省,添加了如下两个(后面会谈到):

//移动构造函数
Empty(Empty &&x){}
//移动赋值
Empty & operator=(Empty &&x)

赋值函数:

void operator=(const Int &it)
{
	this->value = it.value;
}

我们可以从函数的本质上看出这个函数不能完成连续赋值,obja = objb = objc

  • obja = objb.operator=(objc)
  • obja = operator=(&objb,objc)

因为从右往左赋值时,不能将无类型赋值给一个对象。
改写为:

Object & operator=(const Object &it) 
{
	if(this != &it)//c = c这种情况需要特殊处理这里的&不是引用,是取地址符
	{
		this->value = it.value;
	}
	return *this;

	//c = a = b;
	//c.operator=(a.operator=(b));
	//operator=(&c,operator=(&a,b));
}
  • *this代表主函数中的一个对象,不受该函数的影响,因此可以以引用返回,由此也不会产生其他对象用来过渡
上一篇:C++程序设计兼谈对象模型(侯捷)【二】


下一篇:C++ Primier Plus(第六版) 第十一章 使用类 编程练习答案