标题:深拷贝和浅拷贝
先放上我们的基类:
#include <iostream>
using namespace std;
class person
{
public:
person()
{
cout << "person的默认构造函数调用" << endl;
}
person(int a, int height)
{
Age = a;
Height = new int(height);
cout << "person的有参构造函数调用" << endl;
}
~person()
{
//释放堆区数据
if (Height != NULL)
{
delete Height;
Height = NULL;
}
cout << "person的析构构造函数调用" << endl;
}
int Age;
int* Height;
};
int main()
{
person p(10, 20);
person p1(p);
cout << "p的年龄" << p.Age << endl;
cout << "p的身高" << *p.Height << endl;
cout << "p身高的地址" << p.Height << endl;
cout << "p1的年龄" << p1.Age << endl;
cout << "p1的身高" << *p1.Height << endl;
cout << "p1身高的地址" << p1.Height << endl;
}
有同学就要问了,为什么我们这里身高是int *,因为上面我们的height是new出来的,new返回的是一个指针。用这个例子来解释深拷贝。所以我们Height设置为 int *。
概念:
浅拷贝:简单的赋值拷贝操作。简单来说就是编译器调用我们的拷贝构造函数进行简单的赋值操作
深拷贝:在堆区重新申请空间,进行拷贝操作。后面详解。
浅拷贝不说了,就是普通的拷贝,上一篇文章讲了,今天说清楚说深拷贝。
深拷贝:
首先解释个东西,cout<<“p的身高”<<*p.Height <<endl;为什么是 * Height 因为我们设置的身高是一个指针所以我们必须要解引用。
我们原来一直都在说析构函数,但是我们也就是调用了一下,并没有真正的使用起来它,甚至还使用的很少见到他的实际作用!这里的析构代码就是释放我们堆区的数据,就是我们new出来的指针。因为一个new必须对应一个delete!所以我们必须析构函数中进行delete,不进行就容易造成内存泄漏。下面为什么还要将Height=NULL这个操作,是为了防止野指针。
首先这个代码我们都能看出我们没有提供拷贝函数,所以系统自动提供拷贝函数。我们来看一张图片:
很明显的拷贝成功了。没出现拷贝构造函数调用这些话是因为我们根本没写这句话,这次我们使用的是系统提供的拷贝函数。
我们先来看看从图中我们能获得什么信息:
1.年龄没问题
2.身高没问题
3.地址都一样
4.应该输出两次析构函数被调用但为什么只出现了一次。
出现问题了吧,这就是我们调用后发生的问题。少出现了一次析构函数为什么会发生这样的问题?
既然是析构函数的问题,那我们就来看看析构函数。
很完美对不对,不仅仅释放了我们new 出来的空间,而且在释放之前判断了是不是空指针,并且最后还将这个指针置空,完全没有问题啊,那我们接着往下看。
我们前面说过,在销毁前会自动调用析构函数。这里少调用了一次,很明显就是有一次销毁的时候出现了问题。按照我们“完美”的设想来跟踪这段身高代码的逻辑你就知道了问题所在:
在栈区创造p这个对象----->输入身高的值----->调用有参构造函数----->在堆区开辟p的身高指针----->将开辟出来的指针赋值给Height这个指针----->输出p的年龄,身高,身高的地址----->在栈区创建p1这个对象----->使用拷贝构造对这个对象进行"赋值"操作(为什么标红我后面会解释)----->然后输出p1的年龄,身高,身高的地址----->(因为创建在栈区嘛,先进后出)我们调用p1的析构函数,释放开辟在栈区的数据和开辟在堆区的数据----->我们调用p的析构函数,释放开辟在栈区的数据和开辟在堆区的数据。
完整的路径就这样。问题出现在最后一步,仔细观察的朋友已经联系起来了,最开始我们都知道身高这个指针是开辟出来的,但是你有没有发现p和p1的身高指针的地址完全相同!所以我们在前面那个赋值两个字标红了,完完全全的相同。所以我们就认为他们享用的是同一片地址,都是最开始那个new出来的那个指针。我们是只开辟了一个内存空间!但是我们代码运行轨迹的最后两部分却释放了两次!开辟一次释放两次!是不是我们就重复释放了我们开辟出来的空间!p1中的Height先释放了对吧,p还没有释放。是不是p中的Height还没有释放但还指向一片已经释放了的空间!
所以我们总结如下:
1.内存空间的重复释放
2.产生悬空指针
3.浅拷贝是“逐字节拷贝”就是想都不想,直接“搬过去”。
看到这个位置,想的比较深入的同学就会问了,我指针都是空了,我第二次根本不进入这个判断啊,为什么还会执行第二次释放?
这下知道了吧,如果还不懂,我们再来看代码:(也就devc++不会报错了,小提示:我这个写法是错的,因为p1是悬空指针,这么做是为了给大家验证,少用devc++,debug能力太差了)
这个代码里虽然我们的p已经置空了,但是我们的p释放后p1还是指向的是原来的那个位置!
那么如何解决呢?问题出现在了堆区的释放上面,那么我们就不能让他重复释放不久解决了吗?所以我们引入了深拷贝这个概念
深拷贝:
就很简单了,我们重新申请一片内存,我不和你用同一片区域,但是我们内存中存储的值是一样的。看到这个位置,我们举个形象的例子:浅拷贝就是一条船上的人,你翻船了我也跑不掉。那我深拷贝就是我觉得你的船还可以,所以我造一艘和你一模一样的船。我们两个互不干扰。
所以问题就出现在了造船这个环节----->自己写拷贝函数并且并且开辟不一样的堆区内存。
很简单直接上代码:
person(const person & p)
{
cout << "拷贝构造函数的调用" << endl;
Age = p.Age;
//Height=p.Height;刚刚编译器默认提供的就是他,罪魁祸首,敌人。
Height = new int(*p.Height);
}
这次就ok了,我们开辟的位置不一样了。
这次就没问题了,注意了开辟的地址也不一样!我们再来回味一下浅拷贝:
地址一样吧。
以上就是本人对深拷贝和浅拷贝的个人理解,我把自己在学习过程中产生的所有问题都通过语言和图片意义解释了,希望文章对大家有帮助。如果有错误,欢迎指正。有任何问题也可以在评论区留言和私聊本人。