本系列文章主要介绍C++编程语言中类对象的赋值操作和复制操作,以及两者之间的区别,另外还会介绍“深拷贝”与“浅拷贝”的相关知识。
本文为系列文章的第三篇,主要介绍C++编程语言中的“深拷贝”和“浅拷贝”,以及赋值运算符的重载、拷贝构造函数的重载的相关知识。
1 浅拷贝
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;
2.3 总结
从上述两个示例代码可以看出,支持深拷贝的重载赋值运算符和重载拷贝构造函数相似,但两者也存在以下区别:
- 重载赋值运算符的返回值需要是对象的引用,以进行链式赋值(obj3 = obj2 = obj1);而重载拷贝构造函数因为属于构造函数的一种,所以不需要返回值;
- 重载赋值运算符要释放掉对象自身的heap空间(如果存在的话),以避免内存泄漏;而重载拷贝构造函数无需如此,因为拷贝构造函数函数是在创建(并初始化)对象时调用的,对象此时还没有分配heap空间;
- 如果在重载赋值运算符和重载拷贝构造函数都可以解决问题时,建议选择重载拷贝构造函数,因为后者貌似坑少一些:)。