我们在程序中常常需要把一些数据复制一份出来备作它用。对于只有基本类型变量的程序来说,这是轻而易举就能做到的——新建一个临时变量,用一句赋值语句就能完成。但如果它是一个有着许许多多成员数据的对象,这就会非常麻烦。最要命的是,那些成员数据还是私有的,根本无法直接访问或修改。那么这时候,我们怎么“克隆”出一个和原来的对象相同的新对象呢?
我们知道,构造函数是可以带上参数的,这些参数可以是整型、字符型等等,那么它可不可以是一个对象类型呢?我们把一个原来的对象丢给构造函数,然后让它给我们造一个相同的对象,是否可以呢?下面我们就来试试看:(程序15.4.1)
//node.h
#include <iostream>
using namespace std;
class Node//定义一个链表结点类
{
public:
Node();//构造函数的声明
Node(int i,char c='0');//构造函数重载1
Node(int i,char c,Node *p,Node *n);//构造函数重载2
Node(Node &n);//结点拷贝构造函数,&表示引用
int readi() const;//读取idata
char readc() const;//读取cdata
Node * readp() const;//读取上一个结点的位置
Node * readn() const;//读取下一个结点的位置
bool set(int i);//重载,通过该函数修改idata
bool set(char c);//重载,通过该函数修改cdata
bool setp(Node *p);//通过该函数设置前驱结点
bool setn(Node *n);//通过该函数设置后继结点
private:
int idata;//存储数据保密
char cdata;//存储数据保密
Node *prior;//前驱结点的存储位置保密
Node *next;//后继结点的存储位置保密
};
//未定义的函数与程序15.2.2相同
Node::Node(Node &n)
{
idata=n.idata;//可以读出同类对象的私有成员数据
cdata=n.cdata;
prior=n.prior;
next=n.next;
}
//linklist.h
#include "node.h"//需要使用链表结点类
#include <iostream>
using namespace std;
class Linklist
{
public:
Linklist(int i,char c);//链表类构造函数
Linklist(Linklist &l);//链表拷贝构造函数,&表示引用
bool Locate(int i);//根据整数查找结点
bool Locate(char c);//根据字符查找结点
bool Insert(int i=0,char c='0');//在当前结点之后插入结点
bool Delete();//删除当前结点
void Show();//显示链表所有数据
void Destroy();//清除整个链表
private:
Node head;//头结点
Node * pcurrent;//当前结点指针
};
//未定义的函数与程序15.3相同
Linklist::Linklist(Linklist &l):head(l.head)//调用结点的拷贝构造函数来初始化head
{
cout<<"Linklist cloner running..." <<endl;
pcurrent=l.pcurrent;//指针数据可以直接赋值
}
//main.cpp
#include "Linklist.h"
#include <iostream>
using namespace std;
int main()
{
int tempi;
char tempc;
cout <<"请输入一个整数和一个字符:" <<endl;
cin >>tempi >>tempc;
Linklist a(tempi,tempc);
a.Locate(tempi);
a.Insert(1,'C');
a.Insert(2,'B');
a.Insert(3,'F');
cout <<"After Insert" <<endl;
a.Show();
a.Locate('B');
a.Delete();
cout <<"After Delete" <<endl;
a.Show();
Linklist b(a);//创建一个链表b,并且将链表a复制到链表b
cout <<"This is Linklist b" <<endl;
b.Show();//显示b链表中的内容
a.Destroy();
cout <<"After Destroy" <<endl;
a.Show();
return 0;
}
运行结果:
请输入一个整数和一个字符:
4 G
Node constructor is running...
Linklist constructor is running...
Node constructor is running...
Node constructor is running...
Node constructor is running...
After Insert
4 G
3 F
2 B
1 C
After Delete
4 G
3 F
1 C
Linklist cloner running...
This is Linklist b
4 G
3 F
1 C
After Destroy
4 G
根据程序运行的结果,我们发现输出链表b的内容的确和链表a一样了,并且我们可以得到三个结论:
(1) 拷贝构造函数可以读出相同类对象的私有成员数据;
(2) 拷贝构造函数的实质是把参数的成员数据一一复制到新的对象中;
(3) 拷贝构造函数也是构造函数的一种重载。
我们已经知道构造函数有默认构造函数,其实拷贝构造函数也有默认的拷贝构造函数。所谓默认拷贝构造函数是指,用户没有自己定义拷贝构造函数时,系统自动给出的一个拷贝构造函数。默认拷贝构造函数的功能是将对象的成员数据一一赋值给新创建的对象的成员数据,如果某些成员数据本身就是对象,则自动调用它们的拷贝构造函数或默认拷贝构造函数。
既然上面说到,默认拷贝构造函数已经能够满足我们大多数的需要,那么自定义的拷贝构造函数是否就可以不用存在了呢?我们修改一下程序15.4.1的main.cpp文件,看看拷贝构造函数到底有什么意义:
#include "Linklist.h"
#include <iostream>
using namespace std;
int main()
{
int tempi;
char tempc;
cout <<"请输入一个整数和一个字符:" <<endl;
cin >>tempi >>tempc;
Linklist a(tempi,tempc);
a.Locate(tempi);
a.Insert(1,'C');
a.Insert(2,'B');
a.Insert(3,'F');
cout <<"After Insert" <<endl;
a.Show();
a.Locate('B');
a.Delete();
cout <<"After Delete" <<endl;
a.Show();
Linklist b(a);
cout <<"This is Linklist B" <<endl;
b.Show();
a.Destroy();
cout <<"After Destroy" <<endl;
a.Show();
cout <<"This is Linklist b" <<endl;
b.Show();//关键是在这里加了一条语句,让它显示b链表中的内容
return 0;
}
运行结果:
为什么显示链表b的内容,却导致了严重的错误呢?
这时我们就要来研究一下这个链表的结构了。在这个链表中,成员数据只有头结点head和当前指针pcurrent,所有的结点都是通过new语句动态生成的。而程序15.4.1中的拷贝构造函数仅仅是简单地将头结点head和当前指针pcurrent复制了出来,所以一旦运行a.Destroy()之后,链表a头结点之后的结点已经全部都删除了,而链表b的头结点还傻傻地指向原来a链表的结点。如果这时再访问链表b,肯定就要出问题了。如下所示:
程序15.4.1中的拷贝构造函数仅仅是把成员数据拷贝了过来,却没有把动态申请的资源拷贝过来,我们把这种拷贝称为浅拷贝。相对地,如果拷贝构造函数不仅把成员数据拷贝过来,连动态申请的资源也拷贝过来,我们则称之为深拷贝。
下面我们来看如何实现深拷贝:(程序15.4.2)
//node.h同程序15.4.1
//linklist.h
#include "node.h"//需要使用链表结点类
#include <iostream>
using namespace std;
class Linklist
{
public:
Linklist(int i,char c);//链表类构造函数
Linklist(Linklist &l);//链表深拷贝构造函数
bool Locate(int i);//根据整数查找结点
bool Locate(char c);//根据字符查找结点
bool Insert(int i=0,char c='0');//在当前结点之后插入结点
bool Delete();//删除当前结点
void Show();//显示链表所有数据
void Destroy();//清除整个链表
private:
Node head;//头结点
Node * pcurrent;//当前结点指针
};
//未定义的函数与程序15.3相同
Linklist::Linklist(Linklist &l):head(l.head)
{
cout<<"Linklist Deep cloner running..." <<endl;
pcurrent=&head;
Node * ptemp1=l.head.readn();//该指针用于指向原链表中被复制的结点
while(ptemp1!=NULL)
{
Node * ptemp2=new Node(ptemp1->readi(),ptemp1->readc(),pcurrent,NULL);//新建结点,并复制idata和cdata,思考为何这里不能直接用Node的拷贝构造函数?
pcurrent->setn(ptemp2);
pcurrent=pcurrent->readn();//指向表尾结点
ptemp1=ptemp1->readn();//指向下一个被复制结点
}
}
//main.cpp
#include "Linklist.h"
#include <iostream>
using namespace std;
int main()
{
int tempi;
char tempc;
cout <<"请输入一个整数和一个字符:" <<endl;
cin >>tempi >>tempc;
Linklist a(tempi,tempc);
a.Locate(tempi);
a.Insert(1,'C');
a.Insert(2,'B');
a.Insert(3,'F');
cout <<"After Insert" <<endl;
a.Show();
a.Locate('B');
a.Delete();
cout <<"After Delete" <<endl;
a.Show();
Linklist b(a);//创建一个链表b,并且将链表a复制到链表b
cout <<"This is Linklist b" <<endl;
b.Show();
a.Destroy();
cout <<"After Destroy" <<endl;
a.Show();
cout <<"This is Linklist b" <<endl;
b.Show();//链表a被Destroy之后察看链表b的内容
return 0;
}
运行结果:
请输入一个整数和一个字符:
4 G
Node constructor is running...
Linklist constructor is running...
Node constructor is running...
Node constructor is running...
Node constructor is running...
After Insert
4 G
3 F
2 B
1 C
After Delete
4 G
3 F
1 C
Linklist Deep cloner running...
Node constructor is running...
Node constructor is running...
This is Linklist b
4 G
3 F
1 C
After Destroy
4 G
This is Linklist b
4 G
3 F
1 C
我们看到,现在即使运行a.Destroy()之后,链表b里面的数据仍然能够正常显示。这是因为深拷贝构造函数是真正意义上的复制了链表a,并且使得链表a和链表b各自独立,互不干扰。这才是自定义拷贝构造函数存在的重要意义。