C++ c++ 中深拷贝、浅拷贝以及常见的解决方案

sumu · 2019年08月13日 · 300 次阅读

0⃣ 内存四区

想要理解c++中的深浅拷贝,就必须要对c++中内存分配有一个大致的了解。冯诺依曼架构的计算机中内存主要分为五大部分:代码段堆段栈段数据段BSS段。也就是我们常说的c++内存四区,不是五个部分吗?怎么c++中就是四区呢?对的内存四区只是我们为了学习而简化后的,因为今天我们主要讲解深浅拷贝,为了方面没有这部分基础的小伙伴们学习,下面我就以四区简单给大家解释一下,想要深入了解的可以自行查阅相关资料。

  • 代码区,就是我们程序编译成二进制可执行文件后,你运行程序,操作系统会把二进制文件装载到代码区。
  • 数据区,全局变量,静态变量,常量存储在数据区,程序结束后由操作系统进行管理。
  • 堆区,使用new/malloc分配的内存就是开辟在堆区,需要程序员使用delete/free来释放内存,如果没有释放,程序结束由后操作系统管理。
  • 栈区,局部变量就是存放在栈区,由编译器自动进行分配和释放

所以如果我们在堆区开辟了内存,我们就需要程序中释放这块内存,并且不能重复释放

1⃣ 默认构造函数、默认拷贝构造函数,默认析构函数

对于程序员自定义类类型,

  • 如果程序员没有实现构造函数、拷贝构造函数,编译器会默认提供默认的构造函数、默认拷贝构造函数。
  • 如果程序员没有实现拷贝构造函数,(这里指的是实现了一般构造函数),编译器会默认提供默认拷贝构造函数。
  • 如果程序员实现了拷贝构造函数,编译器不会提供默认的构造函数、默认拷贝构造函数。
  • 如果程序员没有实现析构函数,编译器默认提供默认的析构函数。

2⃣ 深、浅拷贝的引入

首先解释一下什么是深拷贝,就是复制一份,对于值类型,就是把值拷贝过去,所以拷贝后的两个值相同,但是并不是一个东西。

#include <iostream>
using namespace std;

class MyInt{
public:
    int id;
    MyInt(){
        cout<<this<<"调用无参构造函数"<<endl;
        id=1;
    }
    MyInt(MyInt& myint){
        cout<<this<<"调用拷贝构造函数"<<endl;
        this->id = myint.id;
    }
    ~MyInt(){
        cout<<this<<"调用析构函数"<<endl;
    }

};

int main() {
    MyInt myint1;
    MyInt myint2(myint1);
    cout<<"myint1-id:"<<&myint1.id<<endl;
    cout<<"myint2-id:"<<&myint2.id<<endl;
    return 0;
}

运行结果如下:

0x7ffc76fbda10调用无参构造函数
0x7ffc76fbda00调用拷贝构造函数
myint1-id:0x7ffc76fbda10
myint2-id:0x7ffc76fbda00
0x7ffc76fbda00调用析构函数
0x7ffc76fbda10调用析构函数

其中拷贝构造函数中我们使用this->id = myint.id;将id值拷贝过去,这就是深拷贝,因为没有涉及到new所以析构函数中我什么也没有做,到现在为止,所说的深拷贝就是值拷贝,拷贝后的两个元素仅仅是值相同,地址不相同,可以认为是两个完全相同的东西。下面我们再举个例子来说明一下。引出我们的浅拷贝。

#include <iostream>
using namespace std;

class MyInt{
public:
    int* id;
    MyInt(){
        cout<<this<<"调用无参构造函数"<<endl;
        id=new int(2);
    }
    MyInt(MyInt& myint){
        cout<<this<<"调用拷贝构造函数"<<endl;
        this->id = myint.id;
    }
    ~MyInt(){
        cout<<this<<"调用析构函数"<<endl;
        delete id;
    }

};

int main() {
    MyInt myint1;
    MyInt myint2(myint1);
    cout<<"myint1-id:"<<myint1.id<<endl;
    cout<<"myint2-id:"<<myint2.id<<endl;
    return 0;
}

上面这个程序就是将第一个程序中的int id修改为了int * int。而后续操作也都改成了指针操作。那我们运行一下看一看效果,问题来不同的编译器效果可能不一样,vs中可能就报错终止了(我没有亲自测试效果),我用的在线运行提示如下:

0x7ffcfe874ce0调用无参构造函数
0x7ffcfe874cd0调用拷贝构造函数
myint1-id:0x9b1010
myint2-id:0x9b1010
0x7ffcfe874cd0调用析构函数
0x7ffcfe874ce0调用析构函数
*** Error in `./a.out': double free or corruption (fasttop): 0x00000000009b1010 ***

但是我们明显能看到程序其实已经执行完成了,因为调用了无参构造,拷贝构造,也显示出来了两个对象的id的地址,然后也调用了析构函数,那为什么会出错呢,让我们在对比一下和第一个程序的执行结果,我们发现第一个值拷贝中两个对象中id的地址是不一样的,而第二次程序中两个对象的id指向了同一块地址。所以第一段程序中析构函数各自析构了各自的内存,而第二段程序中myint2析构后调用了delete释放了0x9b1010这块内存,而当myint1析构时,重复释放0x9b1010这块内存。这就是浅拷贝产生的根源。

3⃣ 浅拷贝的解决办法

回顾一下浅拷贝的产生原因,首先值类型的拷贝肯定不会出现浅拷贝,浅拷贝就是出现在了指针、引用类型上,浅拷贝就是将地址复制了一份,从而导致两个对象公用一块地址,当其中一个对象析构时,释放了内存,其他引用此内存的对象析构时导致重复内存释放,而产生问题。 理解了浅拷贝产生的原因,那对应的解决办法也就有了,上有政策,下有对策嘛!

方案一:不是直接复制指针导致指向同一块内存,从而重复释放内存导致的嘛,所以我们在拷贝构造函数中,不再直接复制指针,而是先开辟一块不小于原数据的内存,再将原内存中的数据复制过来即可。 show me the code。

#include <iostream>
using namespace std;

class MyInt{
public:
    int* id;
    MyInt(){
        cout<<this<<"调用无参构造函数"<<endl;
        id=new int(2);
    }
    MyInt(MyInt& myint){
        cout<<this<<"调用拷贝构造函数"<<endl;

        this->id = new int;
        *id = *myint.id;
    }
    ~MyInt(){
        cout<<this<<"调用析构函数"<<endl;
        delete id;
    }

};

int main() {
    MyInt myint1;
    MyInt myint2(myint1);
    cout<<"myint1-id:"<<myint1.id<<endl;
    cout<<"myint2-id:"<<myint2.id<<endl;
    return 0;
}

让我们执行以下,看一看效果。

0x7ffcbd980270调用无参构造函数
0x7ffcbd980260调用拷贝构造函数
myint1-id:0x211e010
myint2-id:0x211e030
0x7ffcbd980260调用析构函数
0x7ffcbd980270调用析构函数

现在已经不再提示重复释放内存的错误了,可以明显看到,myint1myint2id的地址已经不再指向同一块内存地址。所以在复制构造函数中重新开辟内存,然后将内存中的数据复制到新开辟的内存中,可以解决浅拷贝问题,这种其实就是深拷贝,最终得到了两份完全相同的数据。

方案二:复制构造函数直接复制地址并没有问题,问题在于析构函数重复释放内存导致的。方案一我们是解决复制导致的指向同一块内存地址的问题,从而析构函数释放各自的内存。而方案二我们则是换了一种思路,我们对指向内存地址的对象个数进行记录,其它析构函数不释放内存,由最后一个指向内存地址的析构函数来释放,这也也解决了内存重复释放的问题。 show me the code。

#include <iostream>
using namespace std;

class MyInt{
public:
    int* id;
    int* counter;
    MyInt(){
        cout<<this<<"调用无参构造函数"<<endl;
        id=new int(2);
        counter = new int(1);
    }
    MyInt(MyInt& myint){
        cout<<this<<"调用拷贝构造函数"<<endl;
        this->id = myint.id;
        this->counter = myint.counter;
        (*counter)++;
    }
    ~MyInt(){
        cout<<this<<"调用析构函数"<<endl;
        cout<<this<<" counter:"<<*this->counter<<endl;
        if(--(*counter)==0){
            delete counter;     
            delete id;
        }
    }

};

int main() {
    MyInt myint1;
    cout<<"myint1-counter:"<<*myint1.counter<<endl;
    MyInt myint2(myint1);
    cout<<"myint1-counter:"<<*myint1.counter<<endl;
    cout<<"myint1-id:"<<myint1.id<<endl;
    cout<<"myint2-id:"<<myint2.id<<endl;
    return 0;
}

然后我们看一下这次的执行效果如何,如下

0x7ffd796dc6d8调用无参构造函数
myint1-counter:1
0x7ffd796dc6b8调用拷贝构造函数
myint1-counter:2
myint1-id:0x2337c30
myint2-id:0x2337c30
0x7ffd796dc6b8调用析构函数
0x7ffd796dc6b8 counter:2
0x7ffd796dc6d8调用析构函数
0x7ffd796dc6d8 counter:1

可以看出来myint1myint2id指向的是同一块内存地址,这里使用int*类型的counter,其实是就是为了复制地址导致myint1myint2counter指向同一块内存,以方便计数的同步。我们只要在复制构造函数中进行地址复制和*counter++操作即可,*counter用来记录复制构造的次数,也就是指向同一块内存地址的元素的个数。我们只要在析构函数中进行判断,如果*counter>1,就说明还有其他元素使用了这块内存,那就不释放内存,如果*counter == 1就说明现在可以释放内存!

4⃣ 总结

首先我们做一下总结:

  • 我们介绍了内存四区,简单介绍引出堆区,需要程序员开辟和释放内存,从而有可能导致重复内存释放问题。
  • 几个小程序引出深拷贝以及浅拷贝,以及浅拷贝产生内存重复释放的问题。
  • 浅拷贝的两种常见的解决方案 1)深拷贝 2)引用计数拷贝。
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册