C++深拷贝和浅拷贝

同类对象之间可以通过赋值运算符=互相赋值。如果没有经过重载,=的作用就是把左边的对象的每个成员变量都变得和右边的对象相等,即执行逐个字节拷贝的工作,这种拷贝叫作“浅拷贝”。

有的时候,两个对象相等,从实际应用的含义上来讲,指的并不应该是两个对象的每个字节都相同,而是有其他解释,这时就需要对=进行重载。

上节我们定义了 String 类,并重载了=运算符,使得 char * 类型的字符串可以赋值给 String 类的对象。完整代码如下:

 
  1. #include <iostream>
  2. #include <cstring>
  3. using namespace std;
  4. class String {
  5. private:
  6. char * str;
  7. public:
  8. String() :str(NULL) { }
  9. const char * c_str() const { return str; };
  10. String & operator = (const char * s);
  11. ~String();
  12. };
  13. String & String::operator = (const char * s)
  14. //重载"="以使得 obj = "hello"能够成立
  15. {
  16. if (str)
  17. delete[] str;
  18. if (s) { //s不为NULL才会执行拷贝
  19. str = new char[strlen(s) + 1];
  20. strcpy(str, s);
  21. }
  22. else
  23. str = NULL;
  24. return *this;
  25. }
  26. String::~String()
  27. {
  28. if (str)
  29. delete[] str;
  30. };
  31. int main()
  32. {
  33. String s;
  34. s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
  35. cout << s.c_str() << endl;
  36. // String s2 = "hello!"; //这条语句要是不注释掉就会出错
  37. s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
  38. cout << s.c_str() << endl;
  39. return 0;
  40. }

对于上面的代码,如果让两个 String 对象相等(把一个对象赋值给另一个对象),其意义到底应该是什么呢?是两个对象的 str 成员变量都指向同一个地方,还是两个对象的 str 成员变量指向的内存空间中存放的内容相同?如果把 String 对象理解为存放字符串的对象,那应该是后者比较合理和符合习惯,而前者不但不符合习惯,还会导致程序漏洞。

按照上面代码中 String 类的写法,下面的程序片段会引发问题:

 
  1. String s1, s2;
  2. s1 = "this";
  3. s2 = "that";
  4. s2 = s1;

执行完上面的第 3 行后,s1 和 s2 的状态如图 1 (a) 所示,它们的 str 成员变量指向不同的存储空间。
 


图1:浅拷贝导致的错误


s2=s1;执行的是浅拷贝。执行完s2=s1;后,s2.str 和s1.str 指向同一个地方, 如图 1 (b) 所示。这导致 s2.str 原来指向的那片动态分配的存储空间再也不会被释放,变成内存垃圾。

此外,s1 和 s2 消亡时都会执行delete[] str;,这就使得同一片存储空间被释放两次,会导致严重的内存错误,可能引发程序意外中止。

而且,如果执行完s1=s2;后 又执行s1 = "some";,则会导致 s2.str 也被释放。

为解决上述问题,需要对做=再次重载。重载后的的逻辑,应该是使得执行s2=s1;后,s2.str 和 s1.str 依然指向不同的地方,但是这两处地方所存储的字符串是一样的。再次重载=的写法如下:

 
  1. String & String::operator = (const String & s)
  2. {
  3. if(str == s.str)
  4. return * this;
  5. if(str)
  6. delete[] str;
  7. if(s.str){ //s. str不为NULL才执行复制操作
  8. str = new char[ strlen(s.str) + 1 ];
  9. strcpy(str, s.str);
  10. }
  11. else
  12. str = NULL;
  13. return * this;
  14. }

经过重载,赋值号=的功能不再是浅拷贝,而是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。这样的拷贝就叫“深拷贝”。

程序第 3 行要判断 str==s.str,是因为要应付如下的语句:

s1 = s1;

这条语句本该不改变s1的值才对。s1=s1;等价于s.operator=(s1);,如果没有第 3 行和第 4 行,就会导致函数执行中的 str 和 s.str 完全是同一个指针(因为形参 s 引用了实参 s1,因此可以说 s 就是 s1)。第 8 行为 str 新分配一片存储空间,第 9 行从自己复制到自己,那么 str 指向的内容就不知道变成什么了。

当然,程序员可能不会写s1=s1;这样莫名奇妙的语句,但是可能会写rs1=rs2;,如果 rs1 和 rs2 都是 String 类的引用,而且它们正好引用了同一个 String 对象,那么就等于发生了s1=s1;这样的情况。

思考题:上面的两个 operator= 函数有什么可以改进以提高执行效率的地方?

重载了两次=的 String 类依然可能导致问题。因为没有编写复制构造函数,所以一旦出现使用复制构造函数初始化的 String 对象(例如,String 对象作为函数形参,或 String 对象作为函数返回值),就可能导致问题。最简单的可能出现问题的情况如下:

 
  1. String s2;
  2. s2 = "Transformers";
  3. String s1(s2);

s1 是以 s2 作为实参,调用默认复制构造函数来初始化的。默认复制构造函数使得 s1.str 和 s2.str 指向同一个地方,即执行的是浅拷贝,这就导致了前面提到的没有对=进行第二次重载时产生的问题。因此还应该为 String 类编写如下复制构造函数,以完成深拷贝:

 
  1. String::String(String & s)
  2. {
  3. if(s.str){
  4. str = new char[ strlen(s.str) + 1 ];
  5. strcpy(str, s.str);
  6. }
  7. else
  8. str = NULL;
  9. }


最后,给出 String 类的完整代码:

纯文本复制
 
  1. class String {
  2. private:
  3. char * str;
  4. public:
  5. String() :str(NULL) { }
  6. String(String & s);
  7. const char * c_str() const { return str; };
  8. String & operator = (const char * s);
  9. String & operator = (const String & s);
  10. ~String();
  11. };
  12. String::String(String & s)
  13. {
  14. if (s.str) {
  15. str = new char[strlen(s.str) + 1];
  16. strcpy(str, s.str);
  17. }
  18. else
  19. str = NULL;
  20. }
  21. String & String::operator = (const String & s)
  22. {
  23. if (str == s.str)
  24. return *this;
  25. if (str)
  26. delete[] str;
  27. if (s.str) { //s. str不为NULL才执行复制操作
  28. str = new char[strlen(s.str) + 1];
  29. strcpy(str, s.str);
  30. }
  31. else
  32. str = NULL;
  33. return *this;
  34. }
  35. String & String::operator = (const char * s)
  36. //重载"="以使得 obj = "hello"能够成立
  37. {
  38. if (str)
  39. delete[] str;
  40. if (s) { //s不为NULL才会执行拷贝
  41. str = new char[strlen(s) + 1];
  42. strcpy(str, s);
  43. }
  44. else
  45. str = NULL;
  46. return *this;
  47. }
  48. String::~String()
  49. {
  50. if (str)
  51. delete[] str;
  52. };
### C++深拷贝浅拷贝的区别及用法 #### 概念区分 在 C++ 编程中,当涉及到对象的状态包含其他对象的引用时,深拷贝浅拷贝的概念显得尤为重要。浅拷贝仅复制指向数据的指针或其他引用类型的变量,而不会创建新的内存区域来存储实际的数据副本;相比之下,深拷贝不仅复制指针或引用本身,还会分配新的堆区空间并将原始数据完全复制过去[^1]。 具体来说: - **浅拷贝**是指简单地将一个对象的内容逐字节复制到一个新对象上。如果该对象内部包含了动态分配的资源(如通过 `new` 创建的对象),那么这些资源只会被其地址值所复制,而不是真正意义上的内容复制[^2]。 - **深拷贝**则意味着不仅要复制对象本身的字段,还需要为那些由原对象管理的所有外部资源重新申请相应的内存,并完成完整的数据迁移过程[^3]。 #### 使用场景分析 根据不同的应用场景需求可以选择适合自己的策略: 1. 当两个独立实例之间不需要共享某些特定成员变量(尤其是涉及动态分配部分),此时应该采用深拷贝机制以确保各自拥有单独的一份数据副本; 2. 如果多个对象可以安全共存于同一块物理存储之上,则可考虑利用效率更高的浅拷贝方法减少不必要的开销[^4]. #### 实现方式探讨 为了正确处理上述两种情况下的行为差异,在定义类时通常需要重载以下几个特殊成员函数之一或者全部三个: - 构造函数:用于初始化新创建的对象; - 赋值运算符(`operator=`):负责已存在目标对象更新源操作数表示的新状态; - 析构函数(destructor) :清理当前实例占用的一切非托管型资源. 下面给出一段示范代码展示如何实现自定义版本中的深拷贝逻辑: ```cpp class MyClass { private: int* data; // 动态分配整数数组 public: // 默认构造函数 MyClass(int size){ this->data = new int[size]; std::fill_n(this->data, size, 0); } // 复制构造函数 - 执行深拷贝 MyClass(const MyClass& other){ this->data = new int[other.getSize()]; std::copy(other.data, other.data + getSize(), this->data); } ~MyClass(){ delete[] data; } int getSize() const {return sizeof(data)/sizeof(*data);} }; ``` 以上例子展示了如何在一个简单的类结构里加入必要的措施防止因不当使用而导致意外后果的发生。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值