代码改变世界

白话C++系列(12)-- 深拷贝与浅拷贝

2016-04-24 22:15  Keiven_LY  阅读(986)  评论(0编辑  收藏  举报

深拷贝与浅拷贝

  前面我们已经学习了拷贝构造函数,但是我们只是学习了拷贝构造函数的声明方法以及何时被自动调用,但是我们还未学习如何来实现拷贝构造函数。这是因为对象的拷贝并没有想象中那么简单,大致分为两种情况:深拷贝浅拷贝。我们先来看下面这个例子的实现过程:

    在这个例子中,我们定义了一个数组的类(Array),在这个类中,定义了一个数据成员(m_iCount),并且定义了构造函数,在其中对数据成员赋了初值5,另外还定义了一个拷贝构造函数。在这个拷贝构造函数是这样实现的,传入的参数是arr,这个参数的数据类型也是Array,所以其肯定也含有数据成员m_iCount,在这个拷贝构造函数中,我们将arr的数据成员m_iCount赋值给本身的m_icount。当我们使用时,先用Array arr1来实例化一个arr1的时候,就会调用到arr1的构造函数,也就是说将arr1中的数据成员m_icount赋了初值5。而我们使用Array arr2 = arr1的时候,也就是用arr1去初始化arr2,这时实例化arr2的时候就会调用到它的拷贝构造函数,拷贝构造函数中的参数arr其实就是arr1,里面代码实现的时候,就相当于将arr1的数据成员m_icount赋值给arr2的数据成员m_icount。

       上面这个例子比较简单,下面将这个例子稍微做点修改,如下:

    在这个例子中,我们新加了一个数据成员,它是int型的指针m_pArr,其在构造函数中,从堆中申请了一段内存,并且指向了申请的这段内存,内存的大小就是m_icount。而拷贝构造函数中,我们将arr的数据成员m_iCount赋值给本身的m_icount,同时将arr的数据成员m_pArr赋值给本身的m_pArr。当我们使用时,先用Array arr1来实例化一个arr1的时候,就会调用到arr1的构造函数,也就是说将arr1中的数据成员m_icount赋了初值5。而我们使用Array arr2 = arr1的时候,也就是用arr1去初始化arr2,这时实例化arr2的时候就会调用到它的拷贝构造函数,于是就将arr1的数据成员m_icount赋值给arr2的数据成员m_icount,将arr1的数据成员m_pArr赋值给arr2的数据成员m_pArr。

    在这两个例子中,有共同的特点,那就是,只是将数据成员的值作了简单的拷贝,我们就把这种拷贝模式称为浅拷贝。但是对于第一个例子来说,使用浅拷贝的方式来实现拷贝构造函数并没有任何问题,而对于第二个例子来说,肯定是有问题的。我们来思考一下,经过浅拷贝之后,对象arr1中的指针和对象arr2中的指针势必会指向同一块内存(因为我们将arr1的数据成员m_pArr赋值给arr2的数据成员m_pArr),这里假设指向的地址是0x00FF00(如下图所示)。

    在这个时候,如果我们先给arr1的m_pArr赋了一些值,也就是说在这段内存中就写了一些值,然后我们再给arr1的m_pArr去赋值的时候,这段内存就会被重写,而覆盖掉了之前给arr1的m_pArr所赋的一些值。这一点还不是最严重的问题,更严重的问题是,当我们去销毁arr1这个对象的时候,我们为了避免内存泄漏,肯定会释放掉m_pArr所指向的这段内存。如果我们已经释放掉了这段内存,我们再去销毁arr2这个对象时,我们肯定也会以同样的方式去释放掉arr2中m_pArr这个指针所指向的这段内存,那么就相当于,同一块内存被释放了两次,那么这种问题肯定是有问题的。面对这种问题,计算机会以崩溃的方式来向你抗议。

    所以我们希望拷贝构造函数所完成的工作是这样的,两个对象的指针所指向的应该是两个不同的内存,拷贝的时候不是将指针的地址简单的拷贝过来,而是将指针所指向的内存当中的每一个元素依次的拷贝过来,这才是我们真正想要的。(如下图所示)

如何想要实现这样一个效果呢?我们需要将代码再做适当修改,如下:

    这段代码与之前的代码的区别在于其拷贝构造函数,其中的m_pArr不是直接赋值arr中的m_pArr,而是先分配一段内存(这段内存分配成功与否,这里没有判断,因为这个不是这里要将的重点),重点是下面的一段for循环语句。我们应该将arr中的m_pArr的每一个元素都拷贝到当前的m_pArr所指向的相应的内存当中去。这样的拷贝方式与之前所讲到的拷贝方式是有本质区别的。

    我们来总结一下,当进行对象拷贝时,不是简单的做值的拷贝,而是将堆中内存的数据也进行了拷贝,那么就称这种拷贝模式为深拷贝

深浅拷贝代码实践

题目描述:

/*  示例要求

1. 定义一个Array类。

数据成员:m_iCount

成员函数:

构造函数、拷贝构造函数,析构函数

        数据成员的封装函数

    要求通过这个例子体会浅拷贝原理

2. 在1的基础上增加一个数据成员:m_pArr

并增加m_pArr地址查看函数

同时改造构造函数、拷贝构造函数和析构函数

要求通过这个例子体会深拷贝的原理和必要性

/* ***************************/

针对第1个要求:

头文件(Array.h

class Array
{
public:
    Array();
    Array(const Array &arr);
    ~Array();
    void setCount(int count);
    int getCount();
private:
    int m_iCount;
};

源程序(Array.cpp

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

Array::Array()
{
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    cout <<"Array(const Array &arr)"<<endl;
}
Array::~Array()
{
    cout <<"~Array()"<< endl;
}
void Array::setCount(int count)
{
    m_iCount = count;
}
int Array::getCount()
{
    return m_iCount;
}

主调函数(demo.h

#include<iostream>
#include<stdlib.h>
#include"Array.h"

using namespace std;

int main()    
{
    Array arr1;
    arr1.setCount(5);

    Array arr2(arr1); //通过Arr1来实例化arr2

    cout <<"arr2.m_iCount"<<" "<< arr2.getCount() << endl;

    system("pause");
    return 0;
}

运行结果:

从运行结果看,第一行打印出的是构造函数,也就是说arr1实例化的时候调用的是构造函数;第二行打印出的是拷贝构造函数,也就是说arr2实例化的时候调用的是拷贝构造函数;第三行打印出的是arr2中m_iCount的值为5,这就说明我们用arr1去实例化arr2的时候,也将arr1中的m_iCount的值给了arr2中的m_iCount。这就是浅拷贝,其原理就是将值直接拷贝过去。但是浅拷贝有的时候会带来一些问题,下面我们来看都带来哪些问题,以及如何解决这些问题。

针对第2个要求:

修改头文件(Array.h

class Array
{
public:
    Array(int count);
    Array(const Array &arr);
    ~Array();
    void setCount(int count);
    int getCount();
    void printAddr(); //新增查看地址函数
private:
    int m_iCount;
    int *m_pArr;    //新增数据成员:m_pArr    
};

修改源程序(Array.cpp

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

Array::Array(int count)
{
    m_iCount = count;
    m_pArr = new int[m_iCount];
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    m_pArr = arr.m_pArr;//这里先用浅拷贝实现方式来看看会有什么后果?
    cout <<"Array(const Array &arr)"<<endl;
}
Array::~Array()
{
    delete []m_pArr;
    m_pArr = NULL;
    cout <<"~Array()"<< endl;
}
void Array::setCount(int count)
{
    m_iCount = count;
}
int Array::getCount()
{
    return m_iCount;
}
void Array::printAddr()
{
    cout <<"m_pArr的值是:"<< m_pArr << endl;
}

修改主调函数(demo.h

#include<iostream>
#include<stdlib.h>
#include"Array.h"

using namespace std;

int main()    
{
    Array arr1(5);

    Array arr2(arr1); //通过Arr1来实例化arr2

    cout<<"arr1中" ;
    arr1.printAddr(); 
    cout<<"arr2中" ;
    arr2.printAddr();

    system("pause");
    return 0;
}

运行结果:

从运行结果来看,我们发现arr1中的m_pArr的值与arr2中的m_pArr的值是一样的,也就是说arr1中的m_pArr与arr2中的m_pArr都指向了同一块内存。此前在析构函数中,我们做了删除工作(delete []m_pArr),这就意味着arr1会删除一次,arr2也会删除一次(因为它们指向的是同一块内存,所以相当于让同一块内存释放了两次),这就一定会造成运行时错误。而这个运行时错误不会再这个时候出现,这是因为我们加了一行(system(“pause”);)代码,在这行代码执行完成后,就会执行相应的析构函数,这个时候就会出现运行时错误,我们来验证一下,即在键盘上敲任意键后,屏幕显示如下:

此时,我们发现程序已经死在这了。我们可以看到程序执行了一遍析构函数(因为打印出了析构函数字样“~Array()”),而第二遍析沟函数未执行出来,这就意味着第二次执行析构函数的时候出现了错误。

那么如何来解决这样的问题呢?这时就必须使用深拷贝来解决这个问题了。我们可以看到,此前我们使用的是浅拷贝,直接赋值的方式来进行拷贝的(m_iCount = arr.m_iCount;m_pArr = arr.m_pArr;//浅拷贝实现方式)。深拷贝的方式则需要在拷贝构造函数中给当前的这个指针先分配一段内存,然后将传入的对象的对应位置的内存拷贝到新申请的这段内存中区。那么我们来修改一下构造函数和拷贝构造函数如下:

Array::Array(int count)
{
    m_iCount = count;
    m_pArr = new int[m_iCount];
    for(int i =0; i < m_iCount; i++)
    {
        m_pArr[i] = i;
    }
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    m_pArr = new int[m_iCount];
    for(int i = 0; i < m_iCount; i++)
    {
        m_pArr[i] = arr.m_pArr[i];
    }
    cout <<"Array(const Array &arr)"<< endl;
}

接着还是用刚刚的主调函数来查看arr1和arr2中m_pArr所指向的地址是不是还是一样?运行结果如下:

从结果我们可以看到,这个时候arr1和arr2中m_pArr的地址已经不相同了,可见它们指向了不同的内存,并且当我们按了任意键后,程序也没有崩溃掉,这是因为arr1和arr2中m_pArr所指向的内存不一样了,所以在调用各自析构函数的时候所释放掉内存位置也不相同,所以能够正常释放掉,也就不会报错或崩溃了。

 

接着我们再来申明一个函数,通过这个函数将之前我们赋的值都打印出来。整个程序如下:

程序框架:

头文件(Array.h

class Array
{
public:
    Array(int count);
    Array(const Array &arr);
    ~Array();
    void setCount(int count);
    int getCount();
    void printAddr(); //新增查看地址函数
    void printArr(); //新增打印函数
private:
    int m_iCount;
    int *m_pArr;    //新增数据成员:m_pArr    
};

源程序(Array.cpp

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

Array::Array(int count)
{
    m_iCount = count;
    m_pArr = new int[m_iCount];
    for(int i =0; i < m_iCount; i++)
    {
        m_pArr[i] = i;
    }
    cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
    m_iCount = arr.m_iCount;
    m_pArr = new int[m_iCount];
    for(int i = 0; i < m_iCount; i++)
    {
        m_pArr[i] = arr.m_pArr[i];
    }
    cout <<"Array(const Array &arr)"<< endl;
}
Array::~Array()
{
    delete []m_pArr;
    m_pArr = NULL;
    cout <<"~Array()"<< endl;
}
void Array::setCount(int count)
{
    m_iCount = count;
}
int Array::getCount()
{
    return m_iCount;
}
voidArray::printAddr()
{
    cout <<"m_pArr的值是:"<< m_pArr << endl;
}
void Array::printArr()
{
    for(int i = 0; i < m_iCount; i++)
    {
        cout << m_pArr[i] << endl;
    }
}

主调程序(demo.cpp

#include<iostream>
#include<stdlib.h>
#include"Array.h"

using namespace std;

int main()    
{
    Array arr1(5);

    Array arr2(arr1); //通过Arr1来实例化arr2

    cout<<"arr1中" ; 
    arr1.printAddr(); 
    cout<<"arr2中" ;
    arr2.printAddr();

    cout<<"arr1中m_pArr中的值为"<< endl;; 
    arr1.printArr(); 
    cout<<"arr2中m_pArr中的值为"<< endl;;
    arr2.printArr();

    system("pause");
    return 0;
}

运行结果:

当我们按下任意键后,仔细看屏幕的话,最后会打印出两行“Array(const Array &arr)”,这是因为最后对象销毁时,arr1和arr2调用了各自的析构函数。