C++ 中深拷贝与浅拷贝

为什么要有深拷贝,浅拷贝

看这样一个类:
类Array实现变长数组,并通过类成员变量m_p管理该数组。

class Array {
public:
	Array(int len); //普通构造函数
	~Array(); //析构函数
public:
	int operator[](int i) const { return m_p[i]; } //获取元素(读操作)
	int &operator[](int i) { return m_p[i]; } //写入元素
	int length() const { return m_len; }
private:
	int m_len;
	int *m_p;
};

Array::Array(int len) :m_len(len)
{
	m_p = (int*)malloc(sizeof(int)*m_len);
}

Array::~Array()
{
	free(m_p);
}

现在想要将类Array 的实例A拷贝到类实例B中,分两种情况:
(1)我希望A和B的m_p成员指向同一个数组(A.m_p==B.m_p)。
(2)我希望A和B所管理的是两个独立的数组(虽然数组的大小,值是一样的,但是m_p不一样,两个数组在内存中的地址不一样)。
为了实现上述两种不同的目的,定义了深拷贝和浅拷贝两种不同的拷贝方式,前者是浅拷贝,后者是深拷贝。但实际上目的(1)通常不是我们想要的,并且会产生一个问题,就是当A和B生命周期都结束时,指针m_p会被free两次,A和B的析构函数中各一次。

什么是深拷贝,浅拷贝

浅拷贝
对于基本的数据类型和简单对象,他们之间的拷贝非常简单,就是按位复制内存,这种默认的拷贝行为就是浅拷贝,这和memcpy()函数的调用效果非常类似。

int a=10;
int b=a;

深拷贝
将对象所持有的其他资源一并拷贝的行为叫做深拷贝,必须显示的定义拷贝构造函数才能达到深拷贝的目的。深拷贝会将原有对象的所有成员变量拷贝给新对象,还会为新对象再分配一块内存,并将原有对象所持有的内存也拷贝过来,这样能保证原有对象和新对象所持有的动态内存都是相互独立的,更改一个对象的数据不会影响另一个对象。

深拷贝的例子比比皆是,标准模板库中的string、vector、stack、map等都是必须使用深拷贝的。

怎么实现深拷贝,浅拷贝

拷贝是在对象的初始化阶段进行的,也就是用其他对象的数据初始化新对象的内存。
什么是初始化,与赋值有什么不同?
在定义的同时进行赋值叫做初始化,定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值。初始化就是首次对内存赋值。

拷贝构造函数

拷贝构造函数只有一个参数,而且必须是当前类的引用,可以是const引用,也可以是非const引用,一般都是用const引用,含义更加明确,并且添加const限制后,可以将const或非const对象传递给形参,因为非const类型可以转换为const类型,但是const类型不能转换为非const类型。

拷贝构造函数的形参为什么必须是引用类型?
如果形参是对象类型,即值传递,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,……这个过程会一直持续下去,陷入死循环。

默认拷贝构造函数
如果类不持有数据指针、动态分配内存、打开文件、网络连接等资源,默认拷贝构造函数就够用了,没有必要再显示定义一个。

以拷贝的方式初始化一个对象时会调用拷贝构造函数。
string 类对象的初始化都是用的拷贝方式。例如:

string s1 = "http://c.biancheng.net";
string s2 = s1;
string s3(s1);
string s4 = s1 + s2;

上面的s1、s2、s3、s4都是使用拷贝方式来初始化的。对于s1表面上看起来是将一个字符串直接赋值给了s1,实际上在内部进行了类型转换,将const char *类型转换为string类型后才赋值。

什么时候会调用拷贝构造函数
用类的一个对象去初始化另一个对象时;
当函数的形参是类的对象时(也就是值传递)
当函数返回值是类的对象时。

什么时候用深拷贝,什么时候浅拷贝?

当一个类拥有指针类型的成员变量时,那么绝大部份情况下就需要深拷贝。因为只有这样才能将指针指向的内容再复制一份出来,让原有对象和新对象相互独立,彼此不影响。
如果创建对象时需要进行一些预处理工作,比如统计创建过的对象数目、记录对象创建的时间等,这时就必须使用深拷贝。

重载=(赋值运算符)

当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;当给一个对象赋值时,会调用重载过的赋值运算符。(赋值本身也是拷贝)

即使我们没有显式的重载赋值运算符,编译器也会以默认的方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。

当类中有指针变量、动态内存分配等,需要显示重载赋值运算符。

最后给出实现文章开头提出的情况(2)深拷贝的代码如下:

#include<iostream>
using namespace std;

//自定义Array类,实现变长数组
class Array{
public:
    Array(int len); //普通构造函数
    Array(Array &arr); //拷贝构造函数
    ~Array(); //析构函数
public:
    int operator[](int i) const {return m_p[i];} //获取元素(读操作)
    int &operator[](int i) {return m_p[i];} //写入元素
    int length() const {return m_len;}
    Array &operator=(const Array &arr); //重载赋值运算符,注意返回类型和形参类型
private:
    int m_len;
    int *m_p;
};

Array::Array(int len):m_len(len)
{
    m_p=(int*)malloc(sizeof(int)*m_len);
}

/*拷贝构造函数(复制构造函数)
作用:
1. 将原有对象的所有成员拷贝给新对象;
2. 为新对象分配新的内存空间;
3. 将原有对象所持有的内存拷贝给新对象。
这样做能保证新对象与原有对象持有的动态内存相互独立,修改一个
对象的数据不会影响另一个对象。
注意拷贝构造函数的形参必须是当前类的引用
*/
Array::Array(Array &arr)
{
    this->m_len=arr.m_len;
    this->m_p=(int*)malloc(sizeof(int)*(this->m_len));
    memcpy(this->m_p,arr.m_p,m_len*sizeof(int));
}

Array::~Array()
{
    free(m_p);
}

//重载赋值运算符
/*如果没有显示的重载赋值运算符,编译器也会以默认的方式重载它。
默认重载的赋值运算符很简单,就是将原有对象的成员变量一一赋值给新对象。
这类似于默认的拷贝构造函数,同理,当类持有其他类似动态内存、数据指针等资源,
必须要显示重载赋值运算符,这样才能将原有对象的所有数据赋值给新对象。
1)operator=()的返回类型是Array &,即当前类的引用,这样可以避免返回
数据时调用拷贝构造函数,还能达到连续赋值的目的
2)operator=()的形参类型是const Array &,这样能避免在传参时调用拷贝
构造函数,还能够同时接受const类型和非const类型的实参;
*/
Array &Array::operator=(const Array &arr)
{
    if(this!=&arr){ //判断是否给同一个对象赋值
        this->m_len=arr.m_len;
        free(m_p);
        this->m_p=(int*)malloc(sizeof(int)*this->m_len);
        memcpy(this->m_p,arr.m_p,m_len*sizeof(int));
    }
    return *this; //返回当前对象,即新对象
}

void display(const Array &arr) 
{
    int len=arr.length();
    for(int i=0;i<len;++i){
        if(i==len-1){
            cout<<arr[i]<<endl;
        }
        else{
            cout<<arr[i]<<" ";
        }
    }
}

int main(int argc,char **argv)
{
    Array arr1(10);
    for(int i=0;i<10;++i){
        arr1[i]=i;
    }

    //定义的同时赋值叫做初始化
    //这里会调用拷贝构造函数
    Array arr2=arr1; 
    //如果不使用深拷贝(也就是不显示定义拷贝构造函数,而使用默认拷贝构造函数),对新对象数据的修改也会影响原有对象
    arr2[5]=100;
    arr2[3]=32;

    display(arr1); //输出0 1 2 3 4 5 6 7 8 9
    display(arr2); //输出0 1 2 32 4 100 6 7 8 9 

    Array arr3(5);
    for(int i=0;i<5;++i){
        arr3[i]=i;
    }
    display(arr3); //输出0 1 2 3 4

    //定义完成后的赋值行为叫做赋值,不是初始化。
    //这里会调用重载赋值运算符
    arr3=arr2; 
    display(arr3); //输出0 1 2 32 4 100 6 7 8 9 

    arr3[2]=10; //修改arr3的数据不会影响arr2的数据
    arr3[4]=200;

    display(arr2); //输出0 1 2 32 4 100 6 7 8 9 
    display(arr3); //输出0 1 10 32 200 100 6 7 8 9 
    return 0;
}

总结

对于类拥有的指针类型的成员变量,浅拷贝只拷贝指针变量的值,而深拷贝将指针指向的内容再复制一份出来,让原有对象和新对象相互独立,彼此不影响。

以上部分内容转载至
https://www.jianshu.com/p/289f8baa7d5c

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
设计并实现一个动态整型数组类Vect,要求: (1)实现构造函数重载,可以根据指定的元素个数动态创建初始值为0的整型数组,或根据指定的内置整型数组动态创建整型数组。 (2)设计拷贝构造函数和析构函数,注意使用深拷贝。 (3)设计存取指定位置的数组元素的公有成员函数,并进行下标越界,若越界则输出“out of boundary”。 (4)设计获取数组元素个数的公有成员函数。 (5)设计用于输出数组元素的公有成员函数,元素之间以空格分隔,最后以换行符结束。 在main函数按以下顺序操作: (1)根据内置的静态整型数组{1,2,3,4,5}构造数组对象v1,根据输入的整型数构造数组对象v2。 (2)调用Vect的成员函数依次输出v1和v2的所有元素。 (3)输入指定的下标及对应的整型数,设置数组对象v1的指定元素。 (4)根据数组对象v1拷贝构造数组对象v3。 (5)调用Vect的成员函数依次输出v1和v3的所有元素。 设计并实现一个动态整型数组类Vect,要求: (1)实现构造函数重载,可以根据指定的元素个数动态创建初始值为0的整型数组,或根据指定的内置整型数组动态创建整型数组。 (2)设计拷贝构造函数和析构函数,注意使用深拷贝。 (3)设计存取指定位置的数组元素的公有成员函数,并进行下标越界,若越界则输出“out of boundary”。 (4)设计获取数组元素个数的公有成员函数。 (5)设计用于输出数组元素的公有成员函数,元素之间以空格分隔,最后以换行符结束。 在main函数按以下顺序操作: (1)根据内置的静态整型数组{1,2,3,4,5}构造数组对象v1,根据输入的整型数构造数组对象v2。 (2)调用Vect的成员函数依次输出v1和v2的所有元素。 (3)输入指定的下标及对应的整型数,设置数组对象v1的指定元素。 (4)根据数组对象v1拷贝构造数组对象v3。 (5)调用Vect的成员函数依次输出v1和v3的所有元素。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值