深拷贝与浅拷贝
上一节中曾提到,拷贝构造函数中若只完成数据成员本身的赋值,称为“浅拷贝”,编译器提供的默认拷贝构造函数也可以完成这样的工作。如果要复制的数据除了属性值本身外,还有附加在属性值上的额外内容,则需要自己编写拷贝构造函数,完成所谓“深拷贝”。如在前面案例中,构造函数中通过new开辟空间并用指针成员记录空间首地址,则在指针成员上附加了新开辟空间的信息,在通过拷贝构造函数初始化新对象时应该为新对象的指针成员分配新的空间,即通过“深拷贝”实现新对象的初始化。
与“浅拷贝”对应,“深拷贝”完成了更复杂的拷贝操作,为了学习“深拷贝”,接下来通过一个案例来了解“浅拷贝”可能引发的错误,如例1所示。
例1
1 #include <iostream>
2 #include <cstring>
3 using namespace std;
4
5 class Car //定义类Car
6 {
7 public:
8 Car(char *con_pcarname, int con_seats); //声明带参数的构造函数
9 Car(Car &con_refcar); //声明拷贝构造函数
10 ~Car(); //声明析构函数
11
12 private:
13 char *m_pCarName; //指针成员,指向存有汽车名称的空间
14 int m_nSeats;
15 };
16 Car::Car(char *con_pcarname, int con_seats) //构造函数定义
17 {
18 int len = strlen(con_pcarname) + 1;
19 m_pCarName = new char[len]; //开辟空间,m_pCarName记录首地址
20
21 //向m_pCarName指向的空间中复制汽车名称
22 strcpy_s(m_pCarName, len, con_pcarname);
23 m_nSeats = con_seats;
24 }
25 Car::Car(Car &con_refcar) //定义拷贝构造函数
26 {
27 cout << "calling copy constructor!" << endl;
28 m_pCarName = con_refcar.m_pCarName; //复制指针值
29 m_nSeats = con_refcar.m_nSeats;
30 cout << "end of copy constructor!" << endl;
31 }
32
33 Car::~Car() //定义析构函数
34 {
35 cout << "destructor is called!" << endl;
36 delete[] m_pCarName; //释放m_pCarName指向的空间
37 }
38 int main()
39 {
40 Car mynewcar("my new car", 4); //调用带参数的构造函数定义类对象
41 Car myseccar(mynewcar); //调用拷贝构造函数定义类对象
42 return 0;
43 }
运行结果如图1所示。
图1 例1运行结果
例1运行结果中显示了信息,但是调用析构函数进行对象资源释放后,程序无法正常终止。下面分析错误原因。
例1中分别调用带参数的构造函数和拷贝构造函数创建两个对象mynewcar,myseccar。mynewcar对象通过带参数的构造函数完成初始化,其中将m_pCarName指向一段new开辟的新空间,之后调用strcpy_s()函数将汽车名称存入。mysecar通过拷贝构造函数创建,在拷贝构造函数中只是对m_pCarName进行了指针值的复制,并没有使其指向独立空间,此时两个不同对象的m_pCarName指向的是同一块空间,情况如图2所示。
图2 两个对象的m_pCarName指向同一个空间
这会导致什么问题呢?在对象使用完毕时,都会调用析构函数进行资源回收,本类的析构函数会使用delete将构造函数中分配的空间进行回收。
对mynewcar对象析构时,因为该对象创建时通过new开辟了空间,析构函数中delete回收空间正确。但当myseccar使用结束时,由于myseccar的m_pCarName与mynewcar指向的是同一块空间,对myseccar进行析构时,会因为对已释放的空间进行二次释放而出错,情况如图3所示。
图3 错误,多次释放同一块空间
出现以上问题的原因在于,创建对象myseccar时使用的拷贝构造函数中仅对指针变量本身进行了赋值操作,而没有让对象的m_pCarName指针指向独立空间。因此对例1代码中的拷贝构造函数进行修改,如例2所示。
例2
1 #include <iostream>
2 #include <cstring>
3 using namespace std;
4 class Car //定义类Car
5 {
6 public:
7 Car(char *con_pcarname, int con_seats); //声明带参数的构造函数
8 Car(Car &con_refcar); //声明拷贝构造函数
9 ~Car(); //声明析构函数
10
11 private:
12 char *m_pCarName; //指针成员,指向存有汽车名称的空间
13 int m_nSeats;
14 };
15
16 Car::Car(char *con_pcarname, int con_seats) //构造函数定义
17 {
18 int len = strlen(con_pcarname) + 1;
19 m_pCarName = new char[len]; //开辟空间,m_pCarName记录首地址
20
21 //向m_pCarName指向的空间中复制汽车名称
22 strcpy_s(m_pCarName, len, con_pcarname);
23 m_nSeats = con_seats;
24 }
25 Car::Car(Car &con_refcar) //定义拷贝构造函数
26 {
27 cout << "calling copy constructor!" << endl;
28 int len = strlen(con_refcar.m_pCarName) + 1;
29 m_pCarName = new char[len]; //m_pCarName指向new开辟的空间
30 strcpy_s(m_pCarName, len, con_refcar. m_pCarName);
31 m_nSeats = con_refcar.m_nSeats;
32 cout << "end of copy constructor!" << endl;
33 }
34 Car::~Car() //定义析构函数
35 {
36 static int i = 0;
37 cout << "destructor is called!" << endl;
38 delete[] m_pCarName; //释放m_pCarName指向的空间
39 if (i == 1)
40 system("pause");
41 i++;
42 }
43
44 int main()
45 {
46 Car mynewcar("my new car", 4); //调用带参数的构造函数定义类对象
47 Car myseccar(mynewcar); //调用拷贝构造函数定义类对象
48 return 0;
49 }
运行结果如图4所示。
图4 例2运行结果
例2中代码25-33行是修改后的拷贝构造函数,其中m_pCarName成员执行new开辟空间,这样不论调用哪个构造函数都能保证每个对象的m_pCarName有独立空间,析构时对于每个对象执行delete操作都正确,程序正常终止。
程序修改后,对象mynewcar、myseccar的m_pCarName指向独立空间,情况如图5所示。
图5 不同的对象的m_pCarName指向不同的空间
在mynewcar对象使用完毕后,调用析构函数只是将该对象中m_pCarName指向的空间释放, myseccar中m_pCarName指向的空间不受影响,情况如图6所示。
图6 mynewcar对象释放自己独有空间
修改了拷贝构造函数后,每个m_pCarName都有其指向的独立空间,这与系统提供的默认的拷贝构造函数只进行数据成员的简单复制有很大差别,这种将所有数据都进行复制的拷贝构造函数称为“深拷贝”。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,比如开辟空间,当这个类的对象发生复制的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。