C++ 的浅拷贝和深拷贝(结构体)
关于浅拷贝和深拷贝这个问题遇上的次数不多,这次遇上整理一下,先说这样一个问题,关于浅拷贝的问题,先从最简单的说起。
假设存在一个结构体:
struct Student { string name; int age; }; int main() { struct Student stu = {"liming", 18}; struct Student stu2 = {"wanger", 20}; stu2 = stu; cout<<"age is : "<< stu2.age <<endl; cout<<"name is :"<< stu2.name<<endl; }
这样可以看到的结果是:
age is : 18
name is :liming
说明此时的拷贝是成功的,此时的结构体可以通过“=”来直接进行赋值操作,但是接下来的问题产生了,假设存在如下的结构体:
struct stu { int i; char c; char* p; }; int main() { struct stu s1,s2; char * str = "rabbit is cute"; s1.i = 345; s1.c = 'y'; s1.p = (char*)str; s2 = s1; printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p); printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p); }
产生的结果是这样的:
s2 345, y, rabbit is cute
s1 ptr: 7934, s2 ptr : 7934
可以看到的是S2 确实得到了S1 传递的值,但是第二句的话却说明这样的一个问题,其实S2和S1的指针p都指向一个内存地址,这又说明了什么?
这说明指针的并没有将内容复制一块给新指针来指向,只是让新指针指向原来的那个内存,这样就相当于,指针在这个复制的过程中只是复制了地址,而不是内容。
原理:
在拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。但是注意缺省的构造函数却是这样的:缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标--浅拷贝。
这就是产生问题的原因了,浅拷贝出现了。。。
用下图来解释这个问题:
在进行对象复制后,事实上s1、s2里的成员指针p都指向了一块内存空间(即内存空间共享了),在s1析构时,delete了成员指针p所指向的内存空间,而s2析构时同样指向(此时已变成野指针)并且要释放这片已经被s1析构函数释放的内存空间,这就让同样一片内存空间出现了“double free” ,从而出错。而浅拷贝还存在着一个问题,因为一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变了。
为了实现深拷贝,往往需要自己定义拷贝构造函数,在源代码里,我们加入自定义的拷贝构造函数如下:
在结构体中加入自己的拷贝构造函数:
struct stu { int i; char c; char* p; stu operator=(stu& stuTmp) { i = stuTmp.i; c = stuTmp.c; p = new char(strlen(stuTmp.p) + 1); strcpy(p, stuTmp.p); return *this; }; }; int main() { struct stu s1,s2; char * str = "rabbit is cute"; s1.i = 345; s1.c = 'y'; s1.p = (char*)str; s2 = s1; printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p); printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p); }
测试demo
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> using namespace std; class stu { public: int i; char c; char* p; stu operator=(stu& stuTmp) { this->i = stuTmp.i; this->c = stuTmp.c; this->p = new char(strlen(stuTmp.p) + 1); for (int i = 0; i < strlen(stuTmp.p); i++) { this->p[i] = stuTmp.p[i]; } //strcpy(pp, stuTmp.p); return *this; }; }; int main() { struct stu s1, s2; char * str = "rabbit is cute"; s1.i = 345; s1.c = 'y'; s1.p = (char*)str; s2 = s1; printf("s2 %d, %c, %s\n", s2.i, s2.c, s1.p); printf("s1 ptr: %d, s2 ptr : %d\n", s1.p, s2.p); cin.get(); }
相当于重载operator=方法,这样还是运行,产生的结果就是这样的:
s2 345, y, rabbit is cute
s1 ptr: 7910, s2 ptr : 1050000
此时s1和s2中的指针p指向了不同的地址,可以打印一下此时这两个指针的内容是否一样,加入一下代码:
printf("s1 ptr: %s, s2 ptr : %s\n ", s1.p, s2.p);
产生的结果是:s1 ptr: rabbit is cute, s2 ptr : rabbit is cute
此时s1和s2中的p指针地址不同,但是指向的内容一致,所以这拷贝成功。
其实类的结构和上面的结构体是类似的,其实可以将结构体看成一个类来处理,结构体也可有自己的构造、析构、重载运算符河函数,可以简单的认为结构体是类的一种形式。
拷贝有两种:深拷贝,浅拷贝
当出现类的等号赋值时,会调用拷贝函数 在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。 但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。 所以,这时,必须采用深拷贝。 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。 简而言之,当数据成员中有指针时,必须要用深拷贝。
建议:
我们在定义类或者结构体,这些结构的时候,最后都重写拷贝构造函数,避免浅拷贝这类不易发现但后果严重的错误产生。