C++那些细节--拷贝构造函数

关于C++拷贝构造函数,一直不是很明白,于是强迫症又发作,一定要搞懂它!!

另外附上参考的文章(这位大神写得实在太棒了,让我瞬间搞懂了这个纠结了很久的问题):

http://blog.csdn.net/lwbeyond/article/details/6202256/


一.简介:

拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的形参必须是引用,但并不限制为const,一般普遍的会加上const限制。此函数经常用在函数调用时用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。


二.自动生成拷贝构造函数:

// 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<<"Destruct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};


int _tmain(int argc, _TCHAR* argv[])
{
	CopyTest t1(1, "t1");
	CopyTest t2 = t1;
	t2.Display();

	system("pause");
	return 0;
}
结果如下:

Construct!
t1 1
请按任意键继续. . .


Q&A

1.咦?这里木有写那个传说中的拷贝构造函数啊!为什么还是实现了拷贝呢?

原因是如果我们不写,编译器会为我们自动生成一个拷贝构造函数(类似构造函数和析构函数,如果不写,编译器也会为我们自动生成)。

2.那既然会为我们生成,我们为什么还要写拷贝构造函数呢?

因为自动生成的拷贝构造函数只能实现浅拷贝,不能进行深拷贝。关于浅拷贝和深拷贝在下面具体写。简单的说,就是系统自带的一些数据类型可以实现拷贝,而一些复杂的,指针等,不能进行拷贝,这样自动生成的拷贝构造函数肯定不能满足我们的要求,所以我们就要自己写一个拷贝构造函数啦!


三.我们自己的拷贝构造函数:

免费不一定适合我们,所以我们还是要自己写一个拷贝构造函数,才能更好的实现需要的功能。看一下,真正的拷贝构造函数是什么样子的东东:
// 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<<"Destruct!"<<endl;
	}

	//拷贝构造函数
	CopyTest(const CopyTest& e)
	{
		name = e.name;
		id = e.id;
		cout<<"Copy Construct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};


int _tmain(int argc, _TCHAR* argv[])
{
	CopyTest t1(1, "t1");
	CopyTest t2 = t1;//或者CopyTest t2(t1);
	t2.Display();

	system("pause");
	return 0;
}
结果:
Construct!
Copy Construct!
t1 1
请按任意键继续. . .

可见,系统调用了我们写的拷贝构造函数。

关于拷贝构造函数的几点注意事项:
1.拷贝构造函数是一种特殊的构造函数,函数名必须与类型名一样,参数必须为一个本类型的引用(添加点其他的东东之后,发现系统就不调这个函数了)。
2.我们一般把这个参数引用写成常量引用,因为我们不希望在拷贝的时候,把原对象改动了...不过,const不是强制的,比如加个计数变量神马功能的时候,还是可能需要改动的。

四.什么时候会调用拷贝构造函数

这也是个很关键的问题,我们只是写了拷贝构造函数,但是我们并没有直接调这个函数,一般都是我们在进行几个操作的时候,自动调用拷贝构造函数的。
一共有三种情况:1.对象通过另一个对象进行初始化时 2.通过传值方式传递参数时 3.通过传值方式返回值时。下面分别来看三种情况:

1.对象通过另一个对象进行初始化时
这个是最好理解的一种方式,直接复制另一个对象的各种属性,来达到创建一个对象的目的。这个从字面上理解我们也会想到会调用拷贝构造函数。
例如:
CopyTest t2 = t1;
//或者CopyTest t2(t1);
通过=赋值时,或者直接按照拷贝构造函数的定义方式来调用,将一个对象的引用传递给拷贝构造函数,就可以通过拷贝的方式生成一个新对象。


2.第二种是通过值传递的方式,将对象作为参数传入函数时,会调用拷贝构造函数
例如:
// 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<<"Destruct!"<<endl;
	}

	//拷贝构造函数
	CopyTest(CopyTest& e)
	{
		name = e.name;
		id = e.id;
		cout<<"Copy Construct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};


//值传递方式传递参数的函数
void PassByValue(CopyTest e)
{
	cout<<"Function is called!"<<endl;
}

int _tmain(int argc, _TCHAR* argv[])
{
	CopyTest t1(1, "t1");
	PassByValue(t1);

	system("pause");
	return 0;
}
结果:
Construct!
Copy Construct!
Function is called!
Destruct!
请按任意键继续. . .

可见,我们并没有明显的对象赋值操作,但是仍然调用了拷贝构造函数。而且,在这个里面,我们并没有使用我们传递进来的参数,但是拷贝构造函数仍然调用了,即生成了一个临时的对象,并且,这个临时对象在函数调用完成后,被析构掉了。

我们来分析一下这个过程,对象以值传递的时候,在函数中的操作都不会影响到原对象,这就说明值传递过程中,函数中操作的对象和传递进来的对象不是同一个对象!而是通过拷贝构造函数生成的一个对象。这个过程类似于:CopyTest t2(t1);,我们传递进来的参数为t1,而根据拷贝构造函数生成了对象t2,与t1属性字段均相同,但不是同一个对象。在操作完成后,会销毁这个临时对象。

3.对象以值传递的方式返回时
当我们从函数中返回一个对象,并且是用值传递的方式时,会调用拷贝构造函数:
例如:
// 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(CopyTest& e)
	{
		name = e.name;
		//为了更好的分辨出哪个是原对象哪个是拷贝出来的对象,这里做了一点小手脚,拷贝时id会+1
		id = e.id + 1;
		cout<<"Copy Construct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};

//值传递方式从函数中返回
CopyTest ReturnByValue()
{
	CopyTest t(1, "test");
	return t;
}

int _tmain(int argc, _TCHAR* argv[])
{
	 ReturnByValue();

	system("pause");
	return 0;
}

结果:
Construct!
Copy Construct!
No1 Destruct!
No2 Destruct!
请按任意键继续. . .

(注意:这里为了能够分辨出哪个是拷贝出来的,哪个是原来的,将拷贝构造函数做了一点手脚,拷贝之后,拷贝出来的对象id+1)
简单分析一下这个过程:
创建一个临时对象t,id为1,然后要返回这个对象,调用拷贝构造函数,生成一个临时对象,t1,类似CopyTest t1(t),生成的对象为t1,id为2。在出了函数作用域时,我们发现,竟然 是先析构的原来的对象id为1的,然后才析构的拷贝构造函数生成的临时对象t1。

五.右值引用与转移语义:

如果我们声明一个对象,然后直接后面跟这个函数,那么这个临时对象的析构函数不会调用。这里发生了什么呢?就是传说中的右值引用与转移语义。所谓右值就是临时对象,即将被销毁的对象,如果我们不使用右值的话,就要重新创建一个对象,调用拷贝构造函数等一大堆东西,然后还要销毁这个临时的对象,但是我们通过右值,直接使用转移语义,一是不用重新创建对象,也不用销毁原来的对象,直接相当于把新的对象指针指向原来的那个要被销毁的对象,这样,大大的提高了效率。
而分割线下面,则是非声明的情况,函数返回值赋值给一个已经存在的对象。可见这里临时对象被销毁了。这个 就没有发生转移,因为是赋值,而不是初始化对象。
// 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(CopyTest& e)
	{
		name = e.name;
		id = e.id + 1;//为了更好的分辨出哪个是原对象,这里做了一点小手脚,拷贝出来的对象id+1
		cout<<"Copy Construct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};


//值传递方式传递参数的函数
void PassByValue(CopyTest e)
{
	cout<<"Function is called!"<<endl;
}

//值传递方式从函数中返回
CopyTest ReturnByValue()
{
	CopyTest t(1, "test");
	return t;
}

int _tmain(int argc, _TCHAR* argv[])
{
	
	CopyTest t = ReturnByValue();
	cout<<"--------------------------------------"<<endl;
	 t = ReturnByValue();


	system("pause");
	return 0;
}

Construct!
Copy Construct!
No1 Destruct!
--------------------------------------
Construct!
Copy Construct!
No1 Destruct!
No2 Destruct!
请按任意键继续. . .


六.深拷贝和浅拷贝


1.浅拷贝
先看个例子,上面我们说过,自动生成的拷贝构造函数不一定能用,这个例子就是最好的印证:
// 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):id(i),name(n)
	{
		pointer = new int(1);
		cout<<"Construct!"<<endl;
	}

	~CopyTest()
	{
		delete pointer;
		cout<<"No"<<id<<" "<<"Destruct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};

void TestFunc()
{
	//函数执行完后,会进行析构操作
	CopyTest t1(1, "test");
	CopyTest t2(t1);
}

int _tmain(int argc, _TCHAR* argv[])
{
	TestFunc();
	system("pause");
	return 0;
}

结果:
恩,崩了...



为啥会崩了呢?我们分析一下:
编译器为我们生成的拷贝构造函数只能进行浅拷贝,所谓浅拷贝,就是只是对对象的数据成员进行简单的赋值,而当对象中包含有动态生成的成员时,我们还是一味的赋值就不正确了。

上面的程序进行浅拷贝时,指针也是简单的赋值了一下pointer = e.pointer,而指针所指向的位置是相同的!!!那么,第一个对象销毁时,指针所指向的位置内存被释放了,第二个对象pointer所指向的位置与第一个相同,那么第二个对象使用该指针时就会出现问题!!!这里面,我们再次delete该对象,所以就崩了...

2.深拷贝
我们想要的不是这种,而是下面这张图的效果:

这张图就是我们想要的深拷贝,那么要想这样,我们就需要重新申请一块内存,然后让拷贝出来的对象的pointer指向该位置,并且将该位置的值赋成与原对象相同即可。
// 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):id(i),name(n)
	{
		pointer = new int(1);
		cout<<"Construct!"<<endl;
	}

	~CopyTest()
	{
		delete pointer;
		cout<<"No"<<id<<" "<<"Destruct!"<<endl;
	}

	//拷贝构造函数
	CopyTest(CopyTest& e)
	{
		name = e.name;
		id = e.id + 1;//为了便于区分,这里把拷贝构造函数做了一点手脚,拷贝出来的东东id+1
		pointer = new int();//重新分配一块内存
		*pointer = *(e.pointer);//给该内存赋值,与原对象值相同
		cout<<"Copy Construct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};

void TestFunc()
{
	//函数执行完后,会进行析构操作
	CopyTest t1(1, "test");
	CopyTest t2(t1);
}

int _tmain(int argc, _TCHAR* argv[])
{
	TestFunc();
	system("pause");
	return 0;
}

结果:
Construct!
Copy Construct!
No2 Destruct!
No1 Destruct!
请按任意键继续. . .

恩,这样就不崩了!我们实现了深拷贝!

总结一下:
所谓浅拷贝就是无脑的将对象中的值拷贝过来,如果对象中存在指针等动态成员,那么浅拷贝就会出现问题。
深拷贝则不然,深拷贝在遇到指针时,会另外申请一块内存,用指针指向,这样就不会出现问题。




七.防止按值传递

通过上面的分析,我们发现如果是按值传递参数或者返回时,调用拷贝构造函数。那么,我们换个角度想一下,如果想要不让按值传递发生,即限制按值传参的发生,即限制拷贝构造函数即可。如果我们不写,系统会给我们生成一个拷贝构造函数,那么我们写一个,把它定义成私有的,不就可以了嘛!!!我靠!太机智了!!!

// C++Test.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
using namespace std;


class CopyTest
{
private:
	string name;
	int id;
	int* pointer;
	//私有的拷贝构造函数
	CopyTest(CopyTest& e);
public:
	CopyTest(int i, string n):id(i),name(n)
	{
		pointer = new int(1);
		cout<<"Construct!"<<endl;
	}

	~CopyTest()
	{
		delete pointer;
		cout<<"No"<<id<<" "<<"Destruct!"<<endl;
	}

	void Display()
	{
		cout<<name<<" "<<id<<endl;
	}
};

void TestFunc()
{
	//函数执行完后,会进行析构操作
	CopyTest t1(1, "test");
	CopyTest t2(t1);
}

int _tmain(int argc, _TCHAR* argv[])
{
	TestFunc();
	system("pause");
	return 0;
}

这样,我们编译一下:

恩,编译的时候出现的错误是最容易发现的,比运行时再调试方便多了....



八.几个注意的地方:

1. 以下函数哪个是拷贝构造函数,为什么?

X::X(const X&);      
X::X(X);      
X::X(X&, int a=1);      
X::X(X&, int a=1, int b=2);  


解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

X::X(const X&);  //是拷贝构造函数      
X::X(X&, int=1); //是拷贝构造函数     
X::X(X&, int a=1, int b=2); //当然也是拷贝构造函数  

2. 一个类中可以存在多于一个的拷贝构造函数吗?
解答:类中可以存在超过一个拷贝构造函数。


class X {   
public:         
  X(const X&);      // const 的拷贝构造  
  X(X&);            // 非const的拷贝构造  
};  



注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.


class X {      
public:  
  X();      
  X(X&);  
};      
  
const X cx;      
X x = cx;    // error  

如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。















  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值