问题描述:C++中的深浅拷贝可谓炙手可热的经典题型之一,是许多公司面试中喜欢提及的问题,对于一般的对象例如:int a=10; int b=20;直接赋值和复制没有什么问题,但是当对象上升为类对象时,其类的内部可能存在各种类型的成员变量,在拷贝过程中就存在了深浅拷贝这一问题。


★大笔一挥匆忙写出这种代码不足为奇,但却会引发许多未曾考虑的问题:

#define _CRT_SECURE_NO_WARNINGS  
#include<iostream>  
#include<cstring>  
using namespace std;
class String
{
public:
String(const char* psz = NULL)
:m_psz(strcpy(new char[strlen(psz ? psz : "") + 1], psz ? psz : ""))     
                           //(psz?psz:"")利用三目运算符的性质解决了空串和非空串的情况  
{
cout << "String()构造" << endl;
}
~String()
{
if (m_psz)
{
m_psz = NULL;
}
cout << "String()析构" << endl;
}
char* Back_str()
{
return m_psz;
}
private:
char* m_psz;
};
int main()
{
String s1("hello");        //构造s1对象并初始化  
String s2(s1);          //利用s1拷贝构造出s2,使得s2的内容与s1一致  
cout << "S1 " << s1.Back_str() << endl;
cout << "S2 " << s2.Back_str() << endl;
return 0;
}


分析:此代码可谓是漏洞百出,也是初学者最易写出的代码,因为没有显示定义拷贝构造函数,所以在用s1拷贝构造s2对象时系统自动调用默认的构造函数,所以这也就引出了浅拷贝的问题。什么是浅拷贝?浅拷贝就是指简单的拷贝赋值,因为调用的是系统的拷贝构造函数,所以只是将s1对象的指针m_psz直接拷贝了过去,而原该指针所指向的内容字符串“hello”并未拷贝,这就使得s2对象中的指针并未分配内存空间存储拷贝的字符串,使得这两个指针的指向为同一块内存空间(字符串“hello”的存储空间)。



该拷贝方式会引发两个严重的问题:

①在析构s2对象时,将该指针所指向的空间释放返还给操作系统;这样使得s1成为野指针,指向了一块此时已经不属于自己的非法空间,在析构s1对象时,必然会引起程序的崩溃。


C++中的深浅拷贝问题_深浅拷贝问题


②这两个指针指向的是同一块空间,若当s1的指针修改其字符串的值时,s2也会跟着修改,它没有拷贝字符串,所以该串两者共用, 一改俱改。


由此来看,为解决浅拷贝的问题,便有了深拷贝的拷贝方式。我们发现,只要指针拷贝的同时,为其正确分配内存空间以存储拷贝来的字符串即可,这样两指针分别都指向自己的空间,不会引发以上浅拷贝带来的问题。所以就须得我们自己定义拷贝构造函数,而不借助系统默认自动调用的拷贝构造函数。

String(const String& obj) : m_psz(strcpy((new char[strlen(obj.m_psz) + 1]), obj.m_psz)){
cout << "String拷贝构造" << endl;
}


★当函数存在对象型的参数或对象型的返回值时都会用到拷贝构造函数。

一般在类中还经常有自主定义的运算符的重载函数,以方便对象间的赋值,例如:String s2; s3=s2;这时系统会自动调用默认的运算符重载函数(opertaor+重载的操作符),此例中调用的原型为void operator=(const String& obj){};

所以我们可以自主定义赋值运算符重载函数:

void String(const String&obj)
{
m_psz = strcpy(new char[strlen(obj.m_psz) + 1], obj.m_psz);
}

这样定义的话有两方面的问题:

①函数没有返回值,所以不支持链式访问,也就是说对于连等的对象赋值则会出错,例如:s3=s2=s1;所以解决这个问题的办法就是利用引用的性质,使得函数返回为对象的引用,此时的返回值变成一个左值进而可接受其他的值。

②这样的代码打眼一看漏洞不少。其一没有考虑自己给自己赋值的情况,其二在赋值完成后不释放以前的存储字符串的内存空间,使得m_psz指向新开辟的这片空间,造成了内存泄漏,其三即便开辟了空间,然后释放了原来的空间,一旦空间开辟失败,又释放了原来的空间,就等于是把m_psz指针“卖了”,既不给你空间,原来的你也找不回来了。


所以综合以上的种种问题,我们不得不将代码进行优化,以尽可能多的考虑各种可能的情况。

String& operator=(const String&obj)    //加引用使得返回值为可接受参数的左值 
{
if (&obj != this)                   //排除了自己给自己赋值的情形  
{
char* tmp = strcpy(new char[strlen(obj.m_psz) + 1], obj.m_psz);
                       //先将开辟的空间由指针tmp指向,若空间分配失败,则会抛出一个异常  
delete[] m_psz;      //释放掉原来的指针所指向的空间  
m_psz = tmp;        //再将新开辟并由tmp指向的空间赋值给m_psz  
}
return *this;
}


其实还有一种更好的方法:

String& operator=(const String& obj)
{
if (&obj != this)
{
String str(obj);
char *tmp = m_psz;
m_psz = str.m_psz;
str.m_psz = tmp;
}
return *this;
}

这种方法巧妙了利用了局部变量的性质,在函数内部利用拷贝构造函数构造了一个str临时对象(出了其作用域会自动调用析构函数),走了一个类似于swap函数的过程,str对象保存了原来的m_psz指向的内容,而m_psz此时保存的由str拷贝构造来的新m_psz指向的值,而后出了作用域str对象被析构,只留下了obj对象的m_psz。


这种方法也称为现代写法,也可借助swap库函数进行简化,其实在掌握了基础的写法后再学习这种既巧妙又高大上的写法,会让一个人的编写代码能力更上一个层次!!!