写在前面
本文解释了什么是浅拷贝、深拷贝。记载了使用深拷贝解决浅拷贝导致内存重复释放的具体方法-重载拷贝构造函数和赋值运算符重载函数。文章最后注明了使用重载拷贝构造函数和赋值运算符重载函数的具体情况。
浅拷贝
浅拷贝又叫值拷贝,本质上来说拷贝的目标对象和源对象用的是同一份实体,只是引用的变量名不同而已,访问的还是同一块内存空间。假设有一个String类,String s1;String s2(s1);在进行拷贝构造的时候将对象s1里的值全部拷贝到对象s2里。此时若类中没有重载拷贝构造函数,当进行对象赋值操作时编译器会调用默认拷贝构造函数,这就是浅拷贝。
//默认拷贝构造函数的原型
STRING( const STRING& s )
{
_str = s._str;
}
//默认赋值运算函数原型
STRING& operator=(const STRING& s)
{
if (this != &s)//不允许自己赋值给自己
{
this->_str = s._str;
}
return *this;
}
当声明一个类有指针成员函数时,或者构造函数中有使用new开辟的空间时,该类对象之间直接赋值会导致:
1、被赋值对象中的指针丢失原始指向的内存,造成内存泄漏问题。
2、对象被销毁时调用析构函数,两次都释放的是同一块内存,导致程序崩溃。
#include "stdafx.h"
#include<iostream>
using namespace std;
class STRING {
public:
STRING(const char* s = "") :_str(new char[strlen(s) + 1])//构造函数中有new开辟的空间
{
strcpy_s(_str, strlen(s) + 1, s);
}
//拷贝构造函数原型
/*STRING(const STRING & s) =
{
this->_str = s._str;
}*/
//重载拷贝构造函数
STRING(const STRING & s)
{
_str = new char[strlen(s._str) + 1];
strcpy_s(_str, strlen(s._str) + 1, s._str);
}
//赋值运算符重载函数
STRING & operator=(const STRING &s)
{
if (this != &s)//不允许自己赋值给自己
{
delete[] _str;//1 销毁自己开辟的内存空间
this->_str = new char[strlen(s._str) + 1];//2 开辟新的内存空间,大小和源对象相同
strcpy_s(this->_str, strlen(s._str) + 1, s._str);//3 将源对象内存中的东西复制到新开辟的内存空间中
}
return *this;
}
//析构函数
~STRING()
{
cout << "~STRING" << endl;
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
void show()
{
cout << this->_str << endl;
}
private:
char * _str;
};
int main()
{
STRING S1("IAM");
STRING S2;
S2.operator=(S1);
STRING S3(S1);//调用的是重载后的拷贝构造函数
STRING S4 = S1;//调用的是重载后的拷贝构造函数
STRING S5;
S5 = S1;//调用的是赋值运算符重载函数
S2.show();
S5.~STRING();
S4.~STRING();
S3.~STRING();
S2.~STRING();
S1.~STRING();
system("pause");
return 0;
}
解决浅拷贝两次释放同一块内存问题需要使用深拷贝。
深拷贝
深拷贝:拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
实现深拷贝需要重载拷贝构造函数和赋值运算符重载函数。
重载拷贝构造函数的传统实现:1、开辟一个和源对象内存大小相同的空间 2、把源对象中的内容复制到新内存空间
赋值运算符重载函数的传统实现:1、释放目标对象用new开辟的内存空间 2、开辟与源对象大小相同的内存空间 3、把源对象的内容拷贝到目标对象中。
//重载拷贝构造函数
STRING(const STRING &s):_str(nullptr)
{
_str = new char[strlen(s._str) + 1];//开辟一个和源对象内存大小相同的空间
strcpy_s(_str , strlen(s._str) + 1 , s._str);//把源对象中的内容复制到新内存空间
}
//赋值运算符重载函数
STRING & operator=(const STRING &s)
{
if (this != &s)//不允许自己赋值给自己
{
delete[] _str;//1 销毁自己开辟的内存空间
this->_str = new char[strlen(s._str) + 1];//2 开辟新的内存空间,大小和源对象相同
strcpy_s(this->_str, strlen(s._str) + 1, s._str);//3 将源对象内存中的东西复制到新开辟的内存空间中
}
return *this;
}
重载拷贝构造函数和赋值运算符重载函数现代实现:1、调用构造函数完成tmp对象的构造和初始化(值拷贝) 2、使用swap函数交换tmp和目标拷贝对象所指向的内容
STRING( const STRING& s ):_str(NULL)
{
STRING tmp(s._str);// 调用了构造函数,完成了空间的开辟以及值的拷贝
swap(this->_str, tmp._str); //交换tmp和目标拷贝对象所指向的内容
}
STRING& operator=(const STRING& s)
{
if ( this != &s )//不让自己给自己赋值
{
STRING tmp(s._str);//调用构造函数完成空间的开辟以及赋值工作
swap(this->_str, tmp._str);//交换tmp和目标拷贝对象所指向的内容
}
return *this;
}
ps:使用new初始化对象的指针成员时必须特别小心。具体地说,应当这样做。
1)如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
2)new和delete必须相互兼容。new对应于delete,new[]对应于delete[]。
3) 如果在创建对象的时候采用动态申请(new),那么需要显式的调用类的析构函数。
ps:使用深拷贝时,调用的是重载拷贝构造函数还是赋值运算符重载函数,情况如下:
int main()
{
STRING S1("IAM");
STRING S2;
S2.operator=(S1);//调用的是赋值运算符重载函数
STRING S3(S1);//调用的是重载后的拷贝构造函数
STRING S4 = S1;//调用的是重载后的拷贝构造函数
STRING S5;
S5 = S1;//调用的是赋值运算符重载函数
S2.show();
system("pause");
return 0;
}
参考链接:https://www.cnblogs.com/cxq0017/p/10617313.html