目录
一、浅拷贝和深拷贝定义
1.浅拷贝原理
2.深拷贝原理
二、浅拷贝和深拷贝实现
1.浅拷贝实现
2.深拷贝实现
(1)为什么引用类型成员使用浅拷贝不能实现拷贝构造
(2)如何实现深拷贝
一、浅拷贝和深拷贝定义
拷贝对象时,需要创建相同的字节序、类型、和资源。
1.浅拷贝原理
创建一个新对象, 来接收要重新复制或引用的对象值,要求该对象的所有成员变量全部都不在堆上分配空间。假如果对象的成员变量全部都是内置类型,复制的就是地址;如果对象的成员变量有引用数据类型,复制的就是内存中的地址。对其中一个对象的修改都会影响到另一个对象。
2.深拷贝原理
深拷贝将一个对象完整地从内存中拷贝出来给新对象,从堆中开辟新空间存放新对象。对新对象的修改不会改变原对象,实现两个对象的分离。
二、浅拷贝和深拷贝实现
1.浅拷贝实现
当一个类对象的所有成员变量全部都是内置类型时,可以使用浅拷贝完成拷贝构造:
(1)显式定义拷贝构造函数完成浅拷贝;
(2)如果不显式定义拷贝构造函数,编译器会自动生成默认拷贝构造函数来完成浅拷贝。
如日期类的所有成员变量全部都是内置类型:
#includeusing namespace std; class Date { public: //构造函数 Date(int year = 2022, int month = 4, int day = 8) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } //析构函数:清理资源 ~Date() { cout << "~Date()" << endl;//在析构函数内打印 } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 9, 6);//调用构造函数 Date d4(d1); d1.Print(); d4.Print(); return 0; }
在没有显式定义拷贝构造函数的情况下, d4构造成功了:
2.深拷贝实现
(1)为什么引用类型成员使用浅拷贝不能实现拷贝构造
对于引用类型的成员变量,如果在堆上开辟空间,不显式定义拷贝构造函数的话,会引发两个问题:
①调用析构函数时,这块空间被free了两次
②对其中一个对象进行修改,都会导致另外一个对象被修改
对于stack类,它的成员变量_a是在堆上开辟空间的,如果不显式定义拷贝构造函数,那么会引发程序崩溃:
#include#include using namespace std; typedef int STDataType; class Stack { public: //构造函数 Stack(int capacity = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * 4); _size = 0; _capacity = capacity; } //析构函数:清理资源 ~Stack() { free(_a); _a = nullptr; _size = _capacity = 0; } private: STDataType* _a; int _size; int _capacity; }; int main() { Stack st1; Stack st2(st1); return 0; }
这是因为调构造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类使用深拷贝来拷贝构造对象:
#define _CRT_SECURE_NO_WARNINGS 1 #include#include using namespace std; typedef int STDataType; class Stack { public: //构造函数 Stack(int capacity = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * 4); _size = 0; _capacity = capacity; } //拷贝构造函数 Stack(const Stack& s) :_a(new STDataType[s._capacity]) , _size(s._size) , _capacity(s._capacity) { } //析构函数:清理资源 ~Stack() { free(_a); _a = nullptr; _size = _capacity = 0; } private: STDataType* _a; int _size; int _capacity; }; int main() { Stack st1; Stack st2(st1); return 0; }
st1和st2地址不一样,实现了深拷贝:
②string类使用深拷贝来拷贝构造对象:
#define _CRT_SECURE_NO_WARNINGS 1 #include#include using namespace std; namespace delia { class string { public: //构造函数 string(const char* str = "") { _str = new char[strlen(str) + 1]; strcpy(_str, str); } //传统的拷贝构造函数 string(const string& s) :_str(new char[strlen(s._str) + 1]) { strcpy(_str, s._str); } char& operator[](size_t i) { return _str[i]; } size_t size() { return strlen(_str); } const char* c_str() { return _str; } //析构函数 ~string() { delete[] _str; _str = nullptr; } private: char* _str; }; } int main() { delia::string s1("hello world"); delia::string s2(s1); return 0; }
F10-监视,可以看到s1._str和_s2._str的地址不同,各自拥有各自的空间,实现了深拷贝:
上面实现的是传统的拷贝构造,还有一种现代拷贝构造:
//现代的拷贝构造
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(_str, tmp._str);
}
监视发现,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必须在初始化列表赋值成空指针。
还有现代版的赋值运算符重载:
//赋值运算符重载
string& operator=(string s)
{
swap(_str, s._str);
return *this;
}
int main()
{
gxx::string s3("hello world");
gxx::string s4;
s4 = s3;
return 0;
}
赋值运算符重载,把s3的成员值给了s ,那么s和s3有同样大小的空间和值,s4想要赋值成s,把s和s4进行交换,s的内容交换给了this,s的内容现在是s4原来的内容,s4原来的内容不要了,释放s即可,s的空间释放时,s作为局部对象,出了赋值运算符重载函数作用域就会调用析构函数释放s的空间,把原来s4的内容清理掉了:



