1.1 What
**浅拷贝:**当进行对象拷贝时,只拷贝类中位于 stack 域中的内容,而不会拷贝 heap 域中的内容。
例如,使用类的默认的赋值运算符“=”,或默认的拷贝构造函数时,进行的对象拷贝都属于浅拷贝。这也说明,“浅拷贝”与使用哪种方式(赋值运算符或是拷贝构造函数)进行对象拷贝无关。
1.2 问题
浅拷贝会有一个问题,当类中存在指针成员变量时,进行浅拷贝后,目标对象与源对象的该指针成员变量将会指向同一块 heap 内存区域(而非每个对象单独占用一块内存区域),这就可能导致由于共用该段内存区域而产生内存覆盖、重复释放内存等问题。详情可参考本系列文章第一篇文章的相关内容。
所以,针对带有指针的类对象的拷贝操作,正确的做法是:使两个对象的指针各自指向不同的内存区域,即在拷贝时不是简单地拷贝指针,而是将指针指向的内存中的每一个元素都进行拷贝,由此也就引出了“深拷贝”的概念。
2 深拷贝
**深拷贝:**当进行对象拷贝时,将对象位于 stack 域和 heap 域中的数据都进行拷贝。
前面也提到了,类默认提供的赋值运算符或拷贝构造函数,进行的都是浅拷贝,所以,为了实现对象的深拷贝,需要对赋值运算符或拷贝构造函数进行重载,以达到深拷贝的目的。
2.1 赋值运算符的重载
这里展示一段重载赋值运算符的示例代码,内容如下:
// 重载赋值运算符
ClassA& operaton=(const ClassA& obj)
{
// 适应自赋值(obj = obj)操作
if (this == &obj)
{
return *this;
}
// 新建 heap 空间
int iLength = strlen(str.m_pData);
char* pTemp = new char[iLength + 1];
// 新建 heap 空间成功后,再释放掉已有的 heap 空间
// 保证异常安全性
if (m_pszName != NULL)
{
delete []m_pszName;
m_pszName = NULL;
}
m_pszName = pTemp;
// 拷贝 heap 空间的内容
strcpy(m_pszName, obj.m_pszName);
// 拷贝 stack 域的值
m_nId = obj.m_nId;
return *this;
}
private:
int m_nId;
char* m_pszName;
针对上面的赋值运算符重载函数,说明如下:
- 需要将函数返回值的类型声明为类的引用,同时函数返回实例自身的引用(*this)——只有返回一个引用,才可以允许连续赋值,如 a = b = c;
- 需要将入参的类型声明为常量引用(const 类名& 实例名),避免函数入参的拷贝构造函数调用,提高代码效率;
- 需要考虑自赋值情况;
- 需要释放实例自身已有的内存,避免内存泄漏;
- 需要考虑异常安全性,避免申请内存(new char)失败后,实例中的指针变为空指针,因此需要先执行申请内存操作,内存申请成功后,再释放实例的 heap 空间。
2.2 拷贝构造函数的重载
这里展示一段重载拷贝构造函数的示例代码,内容如下:
// 重载拷贝构造函数,重载后的拷贝构造函数支持深拷贝
ClassA(ClassA &obj)
{
// 拷贝 stack 域的值
m_nId = obj.m_nId;
// 新建 heap 空间
m_pszName = new char[strlen(obj.m_pszName) + 1];
// 拷贝 heap 空间的内容
if (m_pszName != NULL)
{
strcpy(m_pszName, obj.m_pszName);
}
}
private:
int m_nId;
char* m_pszName;