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

来源:互联网 发布:软件开发五个流程 编辑:程序博客网 时间:2024/05/29 18:31

关于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会+1id = 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+1cout<<"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+1pointer = 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&),由编译器根据上下文决定选择哪一个。















1 0
原创粉丝点击