【C++】-- String深浅拷贝详解

简介: 【C++】-- String深浅拷贝详解

一、浅拷贝和深拷贝定义

拷贝对象时,需要创建相同的字节序、类型、和资源。

1.浅拷贝原理

创建一个新对象, 来接收要重新复制或引用的对象值,要求该对象的所有成员变量全部都不在堆上分配空间。假如果对象的成员变量全部都是内置类型,复制的就是地址;如果对象的成员变量有引用数据类型,复制的就是内存中的地址。对其中一个对象的修改都会影响到另一个对象。

2.深拷贝原理

深拷贝将一个对象完整地从内存中拷贝出来给新对象,从堆中开辟新空间存放新对象。对新对象的修改不会改变原对象,实现两个对象的分离。


二、浅拷贝和深拷贝实现

1.浅拷贝实现

当一个类对象的所有成员变量全部都是内置类型时,可以使用浅拷贝完成拷贝构造:

(1)显式定义拷贝构造函数完成浅拷贝;

(2)如果不显式定义拷贝构造函数,编译器会自动生成默认拷贝构造函数来完成浅拷贝。

如日期类的所有成员变量全部都是内置类型:

1. #include<iostream>
2. using namespace std;
3.  
4. class Date
5. {
6. public:
7.  //构造函数
8.  Date(int year = 2022, int month = 4, int day = 8)
9.  {
10.     _year = year;
11.     _month = month;
12.     _day = day;
13.   }
14.  
15.   void Print()
16.   {
17.     cout << _year << "-" << _month << "-" << _day << endl;
18.   }
19.  
20.   //析构函数:清理资源
21.   ~Date()
22.   {
23.     cout << "~Date()" << endl;//在析构函数内打印
24.   }
25.  
26. private:
27.   int _year;
28.   int _month;
29.   int _day;
30. };
31.  
32. int main()
33. {
34.   Date d1(2022, 9, 6);//调用构造函数
35.   Date d4(d1);
36.  
37.     d1.Print();
38.   d4.Print();
39.  
40.   return 0;
41. }

在没有显式定义拷贝构造函数的情况下, d4构造成功了:

2.深拷贝实现

(1)为什么引用类型成员使用浅拷贝不能实现拷贝构造

对于引用类型的成员变量,如果在堆上开辟空间,不显式定义拷贝构造函数的话,会引发两个问题:

①调用析构函数时,这块空间被free了两次

②对其中一个对象进行修改,都会导致另外一个对象被修改

对于stack类,它的成员变量_a是在堆上开辟空间的,如果不显式定义拷贝构造函数,那么会引发程序崩溃:

1. #include <stdlib.h>
2. #include <iostream>
3. using namespace std;
4.  
5. typedef int STDataType;
6.  
7. class Stack
8. {
9. public:
10.   //构造函数
11.   Stack(int capacity = 4)
12.   {
13.     _a = (STDataType*)malloc(sizeof(STDataType) * 4);
14.     _size = 0;
15.     _capacity = capacity;
16.   }
17.  
18.   //析构函数:清理资源
19.   ~Stack()
20.   {
21.     free(_a);
22.     _a = nullptr;
23.     _size = _capacity = 0;
24.   }
25.  
26. private:
27.   STDataType* _a;
28.   int _size;
29.   int _capacity;
30.  
31. };
32.  
33. int main()
34. {
35.   Stack st1; 
36.   Stack st2(st1);
37.  
38.   return 0;
39. }

这是因为调构造s1对象时,_a指向了堆上开辟的空间,由于没有显式定义拷贝构造函数,因此对象st2的成员变量_a拷贝的是st1的成员变量_a指针,即把st1的_a指针的值,拷贝给了st2的_a,那么两个指针的值是一样的,st1的_a和st2的_a指向同一块空间:

造成程序崩溃的原因:调用析构函数,这块空间被free了两次:后定义的先析构,st2先析构,free(_a)就把这块空间释放了,这块空间就被归还给了操作系统,再把_a置空了。再析构st1时,free(_a)还要释放这块空间,同一块空间被释放了两次。

另外,由于共用同一块空间,st1和st2无论谁被修改,都会导致对方也被修改。

(2)如何实现深拷贝

①stack类使用深拷贝来拷贝构造对象:

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include <stdlib.h>
3. #include <iostream>
4. using namespace std;
5. 
6. typedef int STDataType;
7. 
8. class Stack
9. {
10. public:
11.   //构造函数
12.   Stack(int capacity = 4)
13.   {
14.     _a = (STDataType*)malloc(sizeof(STDataType) * 4);
15.     _size = 0;
16.     _capacity = capacity;
17.   }
18. 
19.   //拷贝构造函数
20.   Stack(const Stack& s)
21.     :_a(new STDataType[s._capacity])
22.     , _size(s._size)
23.     , _capacity(s._capacity)
24.   {
25.   }
26. 
27.   //析构函数:清理资源
28.   ~Stack()
29.   {
30.     free(_a);
31.     _a = nullptr;
32.     _size = _capacity = 0;
33.   }
34. 
35. private:
36.   STDataType* _a;
37.   int _size;
38.   int _capacity;
39. 
40. };
41. 
42. int main()
43. {
44.   Stack st1;
45.   Stack st2(st1);
46. 
47.   return 0;
48. }

st1和st2地址不一样,实现了深拷贝:

②string类使用深拷贝来拷贝构造对象:

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include<iostream>
3. using namespace std;
4. 
5. namespace delia
6. {
7.  class string
8.  {
9.  public:
10.         //构造函数
11.     string(const char* str = "")
12.     {
13.       _str = new char[strlen(str) + 1];
14.       strcpy(_str, str);
15.     }
16.       
17.         //传统的拷贝构造函数
18.     string(const string& s)
19.       :_str(new char[strlen(s._str) + 1])
20.     {
21.       strcpy(_str, s._str);
22.     }
23. 
24.     char& operator[](size_t i)
25.     {
26.       return _str[i];
27.     }
28. 
29.     size_t size()
30.     {
31.       return strlen(_str);
32.     }
33. 
34.         const char* c_str()
35.         {
36.             return _str;
37.         }
38. 
39.         //析构函数
40.         ~string()
41.     {
42.       delete[] _str;
43.       _str = nullptr;
44.     }
45.   private:
46.     char* _str;
47.   };
48. }
49. 
50. int main()
51. {
52.   delia::string s1("hello world");
53.   delia::string s2(s1);
54. 
55.   return 0;
56. }

F10-监视,可以看到s1._str和_s2._str的地址不同,各自拥有各自的空间,实现了深拷贝:

上面实现的是传统的拷贝构造,还有一种现代拷贝构造:

1. //现代的拷贝构造
2.    string(const string& s)
3.      :_str(nullptr)
4.    {
5.      string tmp(s._str);
6.      swap(_str, tmp._str);
7.    }

监视发现,s1._str和s2._str地址不同,内容相同,实现了深拷贝:

现代拷贝构造做的事:

(1)将成员初始化成空指针

(2)用原对象成员构造临时对象

(3)交换临时对象和原对象成员

(4)出了拷贝构造函数会自动调用析构函数释放临时对象空间

_str必须在初始化列表赋值成空指针的原因:构造tmp对象时使用s._str初始化,执行swap(_str, tmp._str);来交换this._str和tmp._str的内容,交换完毕后,tmp对象的成员内容为空指针,tmp出了拷贝构造函数作用域就会调用析构函数,会把tmp在堆上申请的空间释放掉,如果_str没有被赋值成空指针,那么_str就是随机值,交换后tmp对象的成员内容也为随机值,而随机值的空间是不能被释放的,会导致不可预知的错误,但是空指针是可以释放的,因此_str必须在初始化列表赋值成空指针。

还有现代版的赋值运算符重载:

1.         //赋值运算符重载
2.    string& operator=(string s)
3.    {
4.      swap(_str, s._str);
5.      return *this;
6.    }
1. int main()
2. {
3.  gxx::string s3("hello world");
4.  gxx::string s4;
5.  s4 = s3;
6. 
7.  return 0;
8. }

赋值运算符重载,把s3的成员值给了s ,那么s和s3有同样大小的空间和值,s4想要赋值成s,把s和s4进行交换,s的内容交换给了this,s的内容现在是s4原来的内容,s4原来的内容不要了,释放s即可,s的空间释放时,s作为局部对象,出了赋值运算符重载函数作用域就会调用析构函数释放s的空间,把原来s4的内容清理掉了:


相关文章
|
2月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
88 5
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
69 2
|
3月前
|
C++ 容器
|
3月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
34 1
|
3月前
|
C++ 容器
|
3月前
|
C++ 容器
|
3月前
|
C语言 C++
深度剖析C++string(中)
深度剖析C++string(中)
62 0
|
3月前
|
存储 编译器 程序员
深度剖析C++string(上篇)(2)
深度剖析C++string(上篇)(2)
51 0
|
3月前
|
存储 Linux C语言
深度剖析C++string(上篇)(1)
深度剖析C++string(上篇)(1)
37 0
|
3月前
|
C++