C++中深拷贝和浅拷贝的问题是很值得我们注意的知识点,如果编程中不注意,可能会出现疏忽,导致bug。本文就详细讲讲C++深浅拷贝的种种。
我们知道,对于一般对象:
int a = 1; int b = 2;
这样的赋值,复制很简单,但对于类对象来说并不一般,因为其内部包含各种类型的成员变量,在拷贝过程中就会出现问题
例如:
#include <iostream> using namespace std; class String { public: String(char str = "") :_str(new char[strlen(str) + 1]) // +1是为了避免空字符串导致出错 { strcpy(_str , str); } // 浅拷贝 String(const String& s) :_str(s._str) {} ~String() { if(_str) { delete[] _str; _str = NULL; } cout<<"~String()"<<endl; } void Display() { cout<<_str<<endl; } private: char *_str; }; void Test() { String s1("hello"); String s2(s1); String.Display(); } int main() { Test(); return 0; }
运行结果:
我们发现,编译通过了,但是崩溃了 = =ll ,这就是浅拷贝带来的问题。
事实是,在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。原型如下:
String(const String& s) {}
但是,编译器提供的缺省函数并不是十全十美的。
缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标--浅拷贝。
用图形象化为:
在进行对象复制后,事实上s1、s2里的成员指针 _str 都指向了一块内存空间(即内存空间共享了),在s1析构时,delete了成员指针 _str
所指向的内存空间,而s2析构时同样指向(此时已变成野指针)并且要释放这片已经被s1析构函数释放的内存空间,这就让同样一片内存空间出现了
“double free”
,从而出错。而浅拷贝还存在着一个问题,因为一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变。所以这不是我们想要的结果,同事也不是真正意义上的复制。
为了解决浅拷贝问题,我们引出深拷贝,即自定义拷贝构造函数,如下:
String(const String& s) :_str(new char[strlen(s._str) + 1]) { strcpy(_str , s._str); }
这样在运行就没问题了。
那么,程序中还有没有其他地方用到拷贝构造函数呢?
答案:当函数存在对象型的参数(即拷贝构造)或对象型的返回值(赋值时的返回值)时都会用到拷贝构造函数。
而拷贝赋值的情况基本上与拷贝复制是一样的。只是拷贝赋值是属于操作符重载问题。例如在主函数若有:String s3; s3 = s2; 这样系统在执行时会调用系统提供的缺省的拷贝赋值函数,原型如下:
void operator = (const String& s) {}
我们自定义的赋值函数如下:
void operator=(const String& s) { if(_str != s._str) { strcpy(_str,s._str); } return *this; }
但是这只是新手级别的写法,考虑的问题太少。我们知道对于普通变量来讲a=b返回的是左值a的引用,所以它可以作为左值继续接收其他值(a=b)=30,这样来讲我们操作符重载后返回的应该是类对象的引用(否则返回值将不能作为左值来进行运算),如下:
String& operator=(const String& s) { if(_str != s._str) { strcpy(_str,s._str); } return *this; }
而上面这种写法其实也有问题,因为在执行语句时,_str 已经被构造已经分
配了内存空间,但是如此进行指针赋值,_str 直接转而指向另一片新new出来的内存空间,而丢弃了原来的内存,这样便造成了内存泄露。应更改为:
String& operator=(const String& s) { if(_str != s._str) { delete[] _str; _str = new char[strlen(s._str) + 1]; strcpy(_str,s._str); } return *this; }
同时,也考虑到了自己给自己赋值的情况。
可是这样写就完善了吗?是否要再仔细思索一下,还存在问题吗?!其实我可以告诉你,这样的写法也顶多算个初级工程师的写法。前面说过,为了保证内存不泄露,我们前面 delete[] _str,然后我们在把new出来的空间给了_str,但是这样的问题是,你有考虑过万一 new 失败了呢?!内存分配失败,m_psz没有指向新的内存空间,但是它却已经把旧的空间给扔掉了,所以显然这样的写法依旧存在着问题。一般高级工程师的写法会是这样的:
String& operator=(const String& s) { if(_str != s._str) { char *tmp = new char[strlen(s._str) + 1]; strcpy(tmp , s._str); delete[] _str; _str = tmp; } return *this; }
这样写就比较全面了。
但是!!!还有元老级别的大师写的更加简便的拷贝构造和赋值函数,我们一睹为快:
<元老级拷贝构造函数>
String(const String& s) :_str(NULL) { String tmp = s._str; swap(_str , tmp._str); }
<元老级赋值函数>
// 1. String& operator=(const String& s) { if(_str != s._str) { String tmp = s._str; swap(_str , tmp._str); } return *this; } // 2. String& operator=(String& s)//在此会拷贝构造一个临时的对象s { if(_str != s._str) { swap(_str ,s._str);// 交换this->_str和临时生成的对象数据成员s._str,离开作用域会自动析构释放 } return *this; }
看出端倪了么?
事实上,这是借助了以上自定义的拷贝构造函数。定义了局部对象 tmp,在拷贝构造中已经为 tmp 的成员指针分配了一块内存,所以只需要将 tmp._str 与this->_str交换指针即可,简化了程序的设计,因为 tmp 是局部对象,离开作用域会调用析构函数释放交换给 tmp._str 的内存,避免了内存泄露。
这是非常值得我们学习和借鉴的。
这是本人对C++深浅拷贝的理解,若有纰漏,欢迎留言指正 ^_^
附注整体代码:
#include <iostream> using namespace std; class String { public: String(char *str = "") :_str(new char[strlen(str) + 1]) { strcpy(_str , str); } // 浅拷贝 String(const String& s) :_str(s._str) {} //赋值运算符重载 //有问题,会造成内存泄露。。。 String& operator=(const String& s) { if(_str != s._str) { strcpy(_str,s._str); } return *this; } // 深拷贝 <传统写法> String(const String& s) :_str(new char[strlen(s._str) + 1]) { strcpy(_str , s._str); } //赋值运算符重载 //一. 这种写法有问题,万一new失败了。。 String& operator=(const String& s) { if(_str != s._str) { delete[] _str; _str = new char[strlen(s._str) + 1]; strcpy(_str,s._str); } return *this; } //二. 对上面的方法改进,先new后delete,如果new失败也不会影响到_str原来的内容 String& operator=(const String& s) { if(_str != s._str) { char *tmp = new char[strlen(s._str) + 1]; strcpy(tmp , s._str); delete[] _str; _str = tmp; } } // 深拷贝 <现代写法> String(const String& s) :_str(NULL) { String tmp = s._str; swap(_str , tmp._str); } //赋值运算符的现代写法一: String& operator=(const String& s) { if(_str != s._str) { String tmp = s._str; swap(_str , tmp._str); } return *this; } //赋值运算符的现代写法二: String& operator=(String& s) //在此会拷贝构造一个临时的对象s { if(_str != s._str) { swap(_str ,s._str);//交换this->_str和临时生成的对象数据成员s._str,离开作用域会自动析构释放 } return *this; } ~String() { if(_str) { delete[] _str; _str = NULL; } cout<<"~String()"<<endl; } void Display() { cout<<_str<<endl; } private: char *_str; }; void Test() { String s1; String s2("hello"); String s3(s2); String s4 = s3; s1.Display(); s2.Display(); s3.Display(); s4.Display(); } int main() { Test(); return 0; }