C/C++学习笔记

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

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

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

上一篇下一篇

猜你喜欢

热点阅读