【C++】浅拷贝和深拷贝

Dear 丶 2024-02-19 13:31 138阅读 0赞

深拷贝与浅拷贝

简单的来说,【浅拷贝】是增加了一个指针,指向原来已经存在的内存。而【深拷贝】是增加了一个指针,并新开辟了一块空间

让指针指向这块新开辟的空间。

【浅拷贝】在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误

浅拷贝

为了形象化说明什么是深拷贝和浅拷贝,我们就先写一个String类

类里面包含【构造函数】,【拷贝构造函数】,【赋值运算符重载】,以及【析构函数】和【输出操作符“<<”的重载】

  1. class String
  2. {
  3. public:
  4. String(const char *pStr = "")
  5. {
  6. if(NULL == pStr)
  7. {
  8. pstr = new char[1];
  9. *pstr = '\0';
  10. }
  11. else
  12. {
  13. pstr = new char[strlen(pStr)+1];//加1,某位是'\0'
  14. strcpy(pstr,pStr);//用拷贝字符串的函数
  15. }
  16. }
  17. String(const String &s)
  18. :pstr(s.pstr)//浅拷贝的问题,指向同一块空间,可能造成释放的错误 ,这是浅拷贝的缺点
  19. {}
  20. String& operator=(const String&s)
  21. {
  22. if(this != &s)
  23. {
  24. delete[] pstr;//将原来所指向的空间释放
  25. pstr = s.pstr;//让pstr重新指向s的pstr所指向的空间(也会导致错误)
  26. }
  27. return *this;
  28. }
  29. ~String()
  30. {
  31. if(NULL != pstr)
  32. {
  33. delete[] pstr;//释放指针所指向的内容
  34. pstr = NULL;//将指针置为空
  35. }
  36. }
  37. friend ostream&operator<<(ostream & _cout,const String &s)
  38. {
  39. _cout<<s.pstr;
  40. return _cout;
  41. }
  42. private:
  43. char *pstr;
  44. };

经过测试之后,在某种情况下是可以正常运行的,在特定情况下是不可以正常的运行的

举一个不能正常运行的例子

  1. int main()
  2. {
  3. String s1("sss");
  4. String s2(s1);
  5. String s3(NULL);
  6. s3 = s1;
  7. cout<<s1<<endl;
  8. cout<<s2<<endl;
  9. cout<<s3<<endl;
  10. return 0;
  11. }

在该例子中,我们有三个String类的对象,s1调用【构造函数】存入字符”sss”

s2调用【拷贝构造函数】来利用s1进行初始化,s3则用【赋值运算符】来进行初始化

黑框框里输出了三个“sss”

然而!

Center

这是为什么呢?

Center 1

通过监视,我们发现他们指向的都是同一块空间,因为地址都是【0x01044570】

在让我们看看【析构函数】

  1. ~String()
  2. {
  3. if (pstr != NULL)
  4. {
  5. delete[] pstr;
  6. pstr = NULL;
  7. }
  8. }

当我们释放s3的时候,可以正常释放

然而当释放s2的时候,由于【s3已经释放过了】,所以s2所指向的这段空间已经不属于s1或者s2了

此时我们调用delete释放的时候,必然会崩溃(毕竟人家本来就不属于你呀)

Center 2

深拷贝

深拷贝以及深浅拷贝的对比

深拷贝和浅拷贝的不同之处,仅仅在于修改了下【拷贝构造函数】,以及【赋值运算符的重载】

对比一下浅拷贝的【拷贝构造函数】和【赋值运算符重载】

  1. String(const String &s)
  2. :pstr(s.pstr)//浅拷贝的问题,指向同一块空间,可能造成释放的错误 ,这是浅拷贝的缺点
  3. {}
  4. String& operator=(const String&s)
  5. {
  6. if(this != &s)
  7. {
  8. delete[] pstr;//将原来所指向的空间释放
  9. pstr = s.pstr;//让pstr重新指向s的pstr所指向的空间(也会导致错误)
  10. }
  11. return *this;
  12. }

" class="reference-link">Center 3

深拷贝完整版

  1. class String
  2. {
  3. public:
  4. String(const char* pStr = "")
  5. {
  6. cout<<"String()"<<endl;
  7. if(NULL == pStr)
  8. {
  9. pstr = new char[1];
  10. *pstr = '\0';
  11. }
  12. else
  13. {
  14. pstr = new char[strlen(pStr)+1];
  15. strcpy(pstr,pStr);
  16. }
  17. }
  18. String(const String &s)
  19. :pstr(new char[strlen(s.pstr)+1])
  20. {
  21. strcpy(pstr,s.pstr);
  22. }
  23. String& operator=(const String &s)
  24. {
  25. if(this != &s)
  26. {
  27. char* tmp = new char[strlen(s.pstr)+1];//pstr;
  28. delete[] pstr;
  29. strcpy(tmp,s.pstr);
  30. pstr = tmp;
  31. }
  32. return *this;
  33. }
  34. ~String()
  35. {
  36. if(NULL != pstr)
  37. {
  38. delete[] pstr;
  39. pstr = NULL;
  40. }
  41. }
  42. private:
  43. char *pstr;
  44. };

除此之外,我们可以简化一下深拷贝的【拷贝构造函数】,【赋值运算符重载】

  1. <span style="color:#000000;">String(const String& s)
  2. :_ptr(NULL)
  3. {
  4. String temp(s._ptr);
  5. std::swap(_ptr, temp._ptr);
  6. }
  7. </span>

在【拷贝构造函数】里用s定义临时变量,临时变量会自动调用构造函数开辟空间

然后用swap这个函数交换两个变量之间的内容

原来的内容在temp,并且出了【拷贝构造函数】就销毁了,避免了内存泄漏

  1. String& operator=(const String& s)
  2. {
  3. if (this != &s)
  4. {
  5. String temp(s);
  6. swap(_ptr, temp._ptr);
  7. }
  8. return *this;
  9. }
  10. String& operator=(const String& s)
  11. {
  12. if(this != &s)
  13. {
  14. String temp(s._ptr);
  15. swap(_ptr,temp._ptr);
  16. }
  17. return *this;
  18. }

[cpp] view plain copy

  1. String& operator=(String temp)
  2. {
  3. swap(_ptr,temp._ptr);
  4. return *this;
  5. }

在【赋值运算符重载】里,也可以用到类似的方法。在第三个方法里,直接传入一个临时变量,连if判断都可以省去了

  1. 解决默认拷贝构造函数的弊端
    类的默认拷贝构造函数只会用被拷贝类的成员的值为拷贝类简单初始化,也就是说二者的p指针指向的内存空间是一致的。以前面TestCls可以知道,编译器为我们默认定义的拷贝构造函数为:

TestCls(const TestCls& testCls)
{
a = testCls.a;
p = testCls.p; //两个类的p指针指向的地址一致。
}
main函数将要退出时,拷贝类t2的析构函数先得到执行,它把自身p指向的堆空间释放了;接下来,t1的析构函数得到调用,被拷贝类t1的析构函数得到调用,它同样要去析构自身的p指向指向的堆空间,但是该空间和t2类中p指向的空间一样,造成重复释放,程序运行崩溃。

解决办法十分简单,自定义拷贝构造函数,里面用深度拷贝的方式为拷贝类初始化:

  1. class TestCls{
  2. public:
  3. int a;
  4. int *p;
  5. public:
  6. TestCls()
  7. {
  8. std::cout<<"TestCls()"<<std::endl;
  9. p = new int;
  10. }
  11. TestCls(const TestCls& testCls)
  12. {
  13. std::cout<<"TestCls(const TestCls& testCls)"<<std::endl;
  14. a = testCls.a;
  15. //p = testCls.p;
  16. p = new int;
  17. *p = *(testCls.p); //为拷贝类的p指针分配空间,实现深度拷贝
  18. }
  19. ~TestCls()
  20. {
  21. delete p;
  22. std::cout<<"~TestCls()"<<std::endl;
  23. }
  24. };
  25. int main(void)
  26. {
  27. TestCls t1;
  28. TestCls t2 = t1;
  29. return 0;
  30. }

编译运行正常:

关于c++拷贝构造函数的深度拷贝和浅拷贝的介绍到这里,其实还可以将它们的地址打印出来看看,不过这一步就不再赘述了。

c++的拷贝构造函数还有一处妙用,就是自定义拷贝构造函数,并设置为private属性,其实现体可以什么都不写,那么这个类将变成一个不可被复制的类了。

总结

【浅拷贝】只是增加了一个指针,指向已存在对象的内存。在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误

【深拷贝】是增加了一个指针,并新开辟了一块空间,让指针指向这块新开辟的空间。

发表评论

表情:
评论列表 (有 0 条评论,138人围观)

还没有评论,来说两句吧...

相关阅读

    相关 C++】拷贝拷贝

    深拷贝与浅拷贝 简单的来说,【浅拷贝】是增加了一个指针,指向原来已经存在的内存。而【深拷贝】是增加了一个指针,并新开辟了一块空间 让指针指向这块新开辟的空间。 【浅拷

    相关 拷贝拷贝

    深拷贝和浅拷贝的理解? 深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。 浅拷贝:将原数组和原对象的引用直接拷贝到新数组和新对象,新对象只

    相关 C++ -拷贝拷贝

    浅拷贝和深拷贝 可能很多人都讲不知道这是深拷贝和浅拷贝是什么东西,现在我们用一个类中的 拷贝构造来说明,好的先看如下一个简单的例 子: define

    相关 拷贝拷贝

    一、浅拷贝 1、浅拷贝和赋值的区别     赋值:当我们把一个对象赋值给一个新变量时,赋的其实是这个对象在栈中的地址,而不是堆中的数据,也就是两个对象指向的是同一个

    相关 C++拷贝拷贝

    C++中类的拷贝有两种:深拷贝,浅拷贝:当出现类的等号赋值时,即会调用拷贝函数 一:两个的区别 1 在未定义显示拷贝构造函数的情况下,系统会调用默认

    相关 拷贝拷贝

    浅拷贝 浅拷贝会创建新对象,其内容非原对象本身的引用,而是原对象内第一层对象的引用。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5n

    相关 C++拷贝拷贝

    简单的说,就是在oop中会涉及到对象的成员中会有指针 如果在复制对象时,只是简单的值复制,则两个对象共用一段内存区域 这是比较危险的。如果一个对象析构函数回收了这段内