深浅拷贝定义
拷贝对象时,需要创建相同的字节序、类型、和资源。
经典的string类问题
// 为了和标准库区分,此处使用String
class String
{
public:
/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
String(const char* str = "")
{
// 构造String类对象时,如果传递nullptr指针,可以认为程序非 if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
}
上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
1.浅拷贝原理
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
创建一个新对象, 来接收要重新复制或引用的对象值,要求该对象的所有成员变量全部都不在堆上分配空间。假如果对象的成员变量全部都是内置类型,复制的就是地址;如果对象的成员变量有引用数据类型,复制的就是内存中的地址。对其中一个对象的修改都会影响到另一个对象。
浅拷贝实现
当一个类对象的所有成员变量全部都是内置类型时,可以使用浅拷贝完成拷贝构造:
(1)显式定义拷贝构造函数完成浅拷贝;
(2)如果不显式定义拷贝构造函数,编译器会自动生成默认拷贝构造函数来完成浅拷贝。
如日期类的所有成员变量全部都是内置类型:
#include<iostream>
using namespace std;
class Date
{
public:
//构造函数
Date(int year = 2024, int month = 5, int day = 3)
{
_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(2024, 10, 1);//调用构造函数
Date d4(d1);
d1.Print();
d4.Print();
return 0;
}
2.深拷贝原理
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
深拷贝将一个对象完整地从内存中拷贝出来给新对象,从堆中开辟新空间存放新对象。对新对象的修改不会改变原对象,实现两个对象的分离。
深拷贝实现
(1)为什么引用类型成员使用浅拷贝不能实现拷贝构造
对于引用类型的成员变量,如果在堆上开辟空间,不显式定义拷贝构造函数的话,会引发两个问题:
①调用析构函数时,这块空间被free了两次
②对其中一个对象进行修改,都会导致另外一个对象被修改
对于stack类,它的成员变量_a是在堆上开辟空间的,如果不显式定义拷贝构造函数,那么会引发程序崩溃:
#include <stdlib.h>
#include <iostream>
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 <stdlib.h>
#include <iostream>
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类的写法
class String
{
public:
String(const char* str = "")
{
// 构造String类对象时,如果传递nullptr指针,可以认为程序非 if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
char* pStr = new char[strlen(s._str) + 1];
strcpy(pStr, s._str);
delete[] _str;
_str = pStr;
}
return *this;
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
现代版String类的写法
class String
{
public:
String(const char* str = "")
{
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(nullptr)
{
String strTmp(s._str);
swap(_str, strTmp._str);
}
// 对比下和上面的赋值那个实现比较好?
String& operator=(String s)
{
swap(_str, s._str);
return *this;
}
/*
String& operator=(const String& s)
{
if(this != &s)
{
String strTmp(s);
swap(_str, strTmp._str);
}
return *this;
}
*/
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
现代拷贝构造做的事
(1)将成员初始化成空指针
(2)用原对象成员构造临时对象
(3)交换临时对象和原对象成员
(4)出了拷贝构造函数会自动调用析构函数释放临时对象空间