笔记十:复制构造函数深拷贝浅拷贝

Posted helenandyoyo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了笔记十:复制构造函数深拷贝浅拷贝相关的知识,希望对你有一定的参考价值。

复制构造函数

定义:

只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数成为复制构造函数。复制构造函数可用于:
1、根据另一个同类型的对象显示或隐式初始化一个对象
2、复制一个对象,将它作为实参传递给一个函数
3、从函数返回时复制一个对象
4、初始化顺序容器中的元素
5、根据元素初始化列表初始化数组元素
——以上定义来自《C++ Primer 中文版 第4版》


浅拷贝/浅复制

第一条中,若一个自定义类对象已经初始化了,并且用该类去初始化另一个同类类型的对象,假设类中存在指针型变量,若没有显示的构造复制构造函数,那么编译器会自动生成一个复制构造函数,此时的复制称为浅拷贝

更准确的定义如下:
在C++中,在用一个对象初始化另一个对象时,只复制了成员,并没有复制资源,使两个对象同时指向了同一资源的复制方式称为浅复制

那么浅拷贝存在的风险是什么呢?

若自定义类中存在指针成员函数,那么:
1、浅拷贝只是复制了指针,使得2个指针指向了同一个地址,这样在对象块结束调用函数析构时,会造成对同一资源的2次析构,即delete 2次,引起程序崩溃。
2、浅拷贝使得2个指针成员指向同一个内存地址,修改其中一个指针指向的值,会改变另一个对象的指针指向的值
3、在内存释放时,作为实参传递的类由于析构不成功,造成内存泄露。
以上来自http://blog.csdn.net/feitianxuxue/article/details/9275979

通过一个实例来反应:
代码:

#include<iostream>

using namespace std;

class A
{
public:
    A()
    {
        data = new char;
        cout << data << endl;
        cout << "默认构造函数" << endl;
    }
    ~A()
    {
        cout << "析构函数" << endl;
        delete data;
        cout << data << endl;
        data = NULL;
    }
private:
    char* data;
};


int main(int argc, char* argv[])
{
    A a;
    A b(a);

    return 0;
}

针对以上代码,进行单步,逐语句的调试:

1、在A a; 处设置断点,采取逐语句F11调试,进入到自定义构造函数中:
这里写图片描述

2、对于对象a——动态分配一个内存给data,此时data的值为0x004084f8,存储data这个值得内存地址为0x002ffbf4 :
这里写图片描述

3、继续执行F11,会发现程序不会再次进入到自定义构造函数,因为此时A b(a) 执行的复制构造函数,由于类没有显示定义,故由编译器自动完成,程序运行到:
这里写图片描述

4、在main函数结束之前,由于对象的声明周期已到,故此时需调用析构函数,且析构函数的顺序是由后往前析构,即对象b在对象a之后定义,那么b在a之前析构。通过观察也可以判断析构顺序,此时存储data变量的地址为0x002ffbe8 与a中存储data的地址不同。
但是a, b中data值均为0x004084f8

这里写图片描述

5、delete释放掉内存资源后,data的值变为0x004084f8<字符串中的字符无法…>:( 个人理解,这里的0x004084f8<字符串中的字符无法…>0x004084f8<妄…>有本质区别,后者内存地址对应着一个实际的内存空间,内存中存储的数据显示乱码。而前者尽管看似为一个内存地址值,但并未对应到一个实际的内存空间,好比一个学号之前是可以对应一个学生的,但是学生信息注销之后,尽管该学号存在,但是无法查找到此人。若理解有误,恳请指正,虚心学习~ ,此时data成为一个垂悬指针。

这里写图片描述

6、避免垂悬指针的存在,将指针指向NULL:

这里写图片描述

7、第一次析构结束:

这里写图片描述

8、第2次析构,即对象a的析构。此时可以观察到data存储的值已经变为0x004084f8<字符串中的字符无法…>,即data是一个野指针了。

这里写图片描述

9、继续析构,则导致程序崩溃:

这里写图片描述

上述调试则解释了浅拷贝可能造成的影响即:
1、同一内存空间析构2次引起程序崩溃
2、一个类成员修改资源,会使得另一个也随之改变
3、第3点有疑问,data指向的内存存储在堆中,但是已经由对象b给释放了,而data是一个局部变量,其储存地址在栈中,程序结束,系统自动收回栈中的资源,那么此时所谓的内存泄露是泄露了哪一部分内存资源呢???表示不太理解,或许我理解有误???

深拷贝/深复制

定义:
当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象另开辟一块新的资源,而不再对拷贝对象中资源的指针或引用进行单纯的赋值。简单地说,即是非共享同一块内存资源,而是重新开辟一块内容,将数据复制到新开辟的内存中。

通过一个实例来反应:

#include<iostream>

using namespace std;

class A
{
public:
    A()
    {
        data = new char;
        cout << data << endl;
        cout << "默认构造函数" << endl;
    }
    A(const A& a)
    {
        data = new char;
        memcpy(data, a.data, sizeof(a.data));
    }
    ~A()
    {
        cout << "析构函数" << endl;
        delete data;
        cout << data << endl;
        data = NULL;
    }
private:
    char* data;
};


int main(int argc, char* argv[])
{
    A a;
    A b(a);

    return 0;
}

采用单步调试F11:
1、程序执行到对象a的实例化:

这里写图片描述

2、进入对象a的构造函数中:

这里写图片描述

3、此时在堆中为data开辟了一个内存,内存地址为0x005584f8

这里写图片描述

4、注意,在浅复制时,程序直接运行到return 0; 。而深复制,F11后进入复制构造函数:

这里写图片描述

这里写图片描述

5、观察此时data指向的内存地址为0x00558d10a.data(0x005584f8)是不同的。memcpy的目的即为复制数据。

这里写图片描述

6、执行对象b的析构,此时data的值为0x00558d10,正好是b对象复制构造中分配的内存空间的地址。

这里写图片描述

7、此时对象b占据的堆中的资源被释放。

这里写图片描述

这里写图片描述

8、第2次析构,即a对象中资源的释放,此时data的值为0x005584f8

这里写图片描述

9、程序没有出现崩溃的状况,表明2次析构成功。
这里写图片描述

那么,什么时候用浅拷贝?什么时候用深拷贝呢?
http://www.cricode.com/753.html 中认为最好使用深拷贝或智能指针。深拷贝相对于浅拷贝来说,会占据额外的内存资源,但是使用更加安全。

以上是关于笔记十:复制构造函数深拷贝浅拷贝的主要内容,如果未能解决你的问题,请参考以下文章

浅拷贝和深拷贝的区别?

深拷贝与浅拷贝

C++深拷贝和浅拷贝细节理解

C++中,啥是深拷贝?啥是浅拷贝?

拷贝构造函数(深拷贝vs浅拷贝)

c++中深拷贝和浅拷贝问题