美文网首页C/C++学习笔记
C++ 拷贝构造函数/深拷贝与浅拷贝

C++ 拷贝构造函数/深拷贝与浅拷贝

作者: 零岁的我 | 来源:发表于2020-06-08 11:08 被阅读0次

    1. C++拷贝构造函数(复制构造函数)

    拷贝和复制是一个意思。

    对于计算机来说,拷贝是指用一份原有的、已经存在的数据创建出一份新的数据,最终的结果是多了一份相同的数据。
    在C++中,拷贝是指用已经存在的对象创建出一个新的对象,从本质上讲,对象也是一份数据,因为它会占用内存。

    拷贝是在对象的初始化阶段进行的,也就是用其他对象的数据初始化新对象的内存。

    什么是初始化,与赋值有什么不同?
    在定义的同时进行赋值叫做初始化,定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值。初始化就是首次对内存赋值,初始化只能有一次,赋值可以有多次。

    拷贝构造函数
    拷贝构造函数只有一个参数,而且必须是当前类的引用,可以是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. 什么时候会调用拷贝构造函数

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

    3. C++深拷贝和浅拷贝(深复制和浅复制)

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

    int a=10;
    int b=a;

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

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

    怎么判断是深拷贝还是浅拷贝

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

    4. C++重载=(赋值运算符)

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

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

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

    深拷贝实验
    记住深拷贝必须显示定义拷贝构造函数才能实现。

    #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;
    }
    
    实验截图

    上述对象之间的初始化与赋值,如果不使用深拷贝,那么对新对象数据的修改也会影响原有对象。

    相关文章

      网友评论

        本文标题:C++ 拷贝构造函数/深拷贝与浅拷贝

        本文链接:https://www.haomeiwen.com/subject/hykazhtx.html