最近一些老铁一直问我深浅拷贝的问题,今天我们就来介绍一下深浅拷贝
在说深浅拷贝构造之前,我们先介绍一下拷贝构造函数的应用场景:
使用另一个同类型的对象来初始化新创建的对象。
浅拷贝
我们在学类和对象时了解到了类的6大默认函数,其中就有拷贝构造函数。
当我们没有自己写拷贝构造函数,编译器自动调用的拷贝构造函数使用的就是浅拷贝。包括三大默认拷贝构造函数。
什么是浅拷贝?
浅拷贝只是一种简单的拷贝,让几个对象公用一个内存,然而当内存销毁的时候,指向这个内存空间的所有指针需要重新定义,不然会造成野指针错误。
一般如果有指针变量的话,就不用浅拷贝,这里涉及到析构的问题,当函数快要结束后会调用一次析构函数,但是如果涉及到指针的话,两个类中的指针指向的是同一片空间地址,那当析构指针时会根据地址多次析构这块空间,会造成内存泄露。
浅拷贝的缺陷
比如两个对象s1,s2 A(s2) s1的话,就是把s1完完全全复制给s2,但是他俩公用一块s1原来的空间,如果说没有引用的话,深浅拷贝都是一样的,只要不改变这个空间里面的内容,改变s2不会对s1有实质性的影响。
但是如果说拷贝的对象的元素里包含引用的话(链表套链表),如果说用到浅拷贝,s2,s1也是同一个引用,相当于汇入一条主路的交叉路口,我从左边能到主路上,也能从右边过去。所以对新对象里的引用里的值进行修改,依然会改变原对象里的列表的值,新对象与原对象并没有完全分离开。
栈对象的例子就十分贴切,
class Stack
{
public:
Stack(int capacity = 4)
{
_ps = (int*)malloc(sizeof(int)* capacity);
_size = 0;
_capacity = capacity;
}
void Print()
{
cout << _ps << endl;// 打印栈空间地址
}
private:
int* _ps;
int _size;
int _capacity;
};
int main()
{
Stack s1;
s1.Print();// 打印s1栈空间的地址
Stack s2(s1);// 用已存在的对象s1创建对象s2
s2.Print();// 打印s2栈空间的地址
return 0;
}
这里我们没有写拷贝构造函数,而是使用编译器默认的拷贝构造函数,实现浅拷贝的工作。
很明显,拷贝出来的s2和s1公用同一片空间,一旦涉及到析构,就肯定会出现错误。
向报这种能打印地址的还好一点,有的直接报错:
class Stu {
public:
Stu() //默认构造函数
{
cout << "u默认构造桉树调用" << endl;
}
Stu(int age, int height)//有参构造函数
{
_age = age;
_height = new int(height); //在堆区开辟内存
cout << "有参构造函数调用" << endl;
}
~Stu()
{
if (_height != NULL) {
delete _height;
_height = NULL;
}
cout << "析构函数的调用" << endl;
}
int _age; //年龄
int* _height; //体重 专门用指针存放
};
int main(void)
{
Stu p1(18, 160);
cout << "--------" << "p1的年龄是" << p1._age << "体重是" << *p1._height << endl;
Stu p2(p1);
cout << "--------" << "p2的年龄是" << p2._age << "体重是" << *p2._height << endl;
return 0;
}
很多时候我们用拷贝构造时就是想再拷贝一个和原来一模一样但是两者没有丝毫牵连的对象。
虽然很多时候浅拷贝就能解决问题,那也有解决不了的问题,那解决不了的问题就需要用到深拷贝了。
深拷贝
为啥会用到深拷贝呢?就是解决浅拷贝不能解决的问题的。
当数据成员出现引用或者是想要两片互不干扰的空间时就需要深拷贝了。
当用浅拷贝时,新对象的指针与原对象的指针指向了堆上的同一块儿内存,新对象和原对象析构时,新对象先把其指向的动态分配的内存释放了一次,而后原对象析构时又将这块已经释放过的内存再释放一次。对同一块动态内存执行2次以上释放的结果是未定义的,所有会导致内存泄漏或程序崩溃。
所以需要深拷贝来解决问题,当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用)时,对象开辟一块新的资源,而不再对拷贝对象中对其他资源的引用的指针或引用进行单纯的赋值。
就比如上面那个报错的实例,如果我们自己手写一个深拷贝,那一点事情没有。
class Stu {
public:
Stu() //默认构造函数
{
cout << "u默认构造桉树调用" << endl;
}
Stu(int age, int height)//有参构造函数
{
_age = age;
_height = new int(height); //在堆区开辟内存
cout << "有参构造函数调用" << endl;
}
//拷贝构造函数
Stu(const Stu& p)
{
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
_age = p._age;
_height = new int(*p._height);
}
~Stu()
{
if (_height != NULL) {
delete _height;
_height = NULL;
}
cout << "析构函数的调用" << endl;
}
int _age; //年龄
int* _height; //体重 专门用指针存放
};
int main(void)
{
Stu p1(18, 160);
cout << "--------" << "p1的年龄是" << p1._age << "体重是" << *p1._height << endl;
Stu p2(p1);
cout << "--------" << "p2的年龄是" << p2._age << "体重是" << *p2._height << endl;
return 0;
}
这里之所以用深拷贝可以是因为new了一大片新空间,然后又在这个空间中拷贝了一份备份,然后析构两次,分别析构的是两片不同空间的内容。所以这个可行。
深浅拷贝的写法
深拷贝:
这是截取vector模拟实现的一部分会代码,没啥具体含义。
//传统写法
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
_start = new T[v.capacity()]; //开辟一块和容器v大小相同的空间
for (size_t i = 0; i < v.size(); i++) //将容器v当中的数据一个个拷贝过来
{
_start[i] = v[i];
}
_finish = _start + v.size(); //容器有效数据的尾
_endofstorage = _start + v.capacity(); //整个容器的尾
}
浅拷贝的常用写法:
Date(const Date& d)// 拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这个是截取原来日期类的拷贝构造的一部分代码,写这两种代码就是让各位 复习一下深浅拷贝的常用写法。