对于C++中的深拷贝与浅拷贝,就类似一个人去模仿一个人
浅拷贝:(影子复制,也称影子克隆),就算怎样去模仿那个人,可是也不是成为本尊,只是与本尊极为相似的一个人。
深拷贝:(深度克隆):不紧复制对象的基本类,同时也复制原对象中的对象.就是说完全是新对象产生的.就像对一个人通过克隆技术,实现他的另一个。
下面我们就对浅拷贝与深拷贝做一个具体的分析吧!
1.管理字符串string
2.深拷贝与浅拷贝中,构造函数与析构函数是一致的,不影响函数的运行。
一、浅拷贝
浅拷贝:当类中有指针对象时,拷贝构造和赋值运算符重载只进行值拷贝,两个对象指向同一个空间,当对象销毁之时,该空间被释放了两次,因此程序崩溃。
下面我们就来抛一段具体的代码来分析吧!
//浅拷贝,,只拷贝内存中的值,如果调用这个函数的话,会使一块内存被释放两次,,从而导致程序崩溃
//导致两个对象共用同一块一块空间
//反例
String(const String &s)
:_pStr(s._pStr)
{}
String&operator=(const String &s)
{
if (this != &s)
{
_pStr = s._pStr;
}
return *this;
}
在此处调用拷贝构造函数之时,由于两个对象甚至更多的对象同时指向同一块内存,因此在释放之时,由于多次释放,因此使得程序崩溃。
对于赋值运算符的重载,由于与拷贝构造函数一致,在上面已经给出分析,因此在这里就不在给出他的分析了。
总结:如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝。
二、深拷贝
深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。
深拷贝:构造s2之时,拷贝一块与s1指向数据一样大的数据块,并将值拷贝下来,这样s1和s2指向各自的数据块,析构时各自释放各自的。如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
下面,上代码
//深拷贝
String(const String& s)//拷贝构造(普通版)
:_pStr(new char[strlen(s._pStr) + 1])//为传递过来的函数开辟空间,防止两块空间共用一块空间
{
strcpy(_pStr, s._pStr);
}
String& operator=(const String& s)//(普通版)1.开辟新空间2.拷贝元素3.释放旧空间
{
if (this != &s)//如若不相等,则对齐进行赋值
{
char *tmp = new char[strlen(s._pStr) + 1];//s传过来就是一个指向第一个元素的指针,开辟新空间
strcpy(tmp, s._pStr);
delete[] _pStr;
_pStr = tmp;
}
return *this;
}
深拷贝与浅拷贝最大的区别就是,在深拷贝之时,会为对象重新分配内存,使其指向新的内存空间,因此使得其在释放之时,不会因为一块空间被释放多次而使得程序崩溃。
深拷贝的简洁版:
String(const String& s)//拷贝构造(简洁版)
:_pStr(NULL)//防止_pStr称为野指针,,为其开辟空间
{
String tmp(s._pStr);//创建临时变量,调用构造函数
swap(_pStr, tmp._pStr);
}
String& operator=(const String& s)
{
if (this != &s)
{
String tmp(s._pStr);//调用构造函数
swap(_pStr, tmp._pStr);
}
return *this;
}
String& operator=( String s)//调用拷贝构造
{
swap(_pStr, s._pStr);
return *this;
}
String& operator=(const String& s)
{
if (this != &s)
{
String tmp(s);
swap(_pStr, tmp._pStr);
}
return *this;
}
总结(深拷贝与浅拷贝)
浅拷贝是指将对象中的数值类型的字段拷贝到新的对象中,而对象中的引用型字段则指复制它的一个引用到目标对象。如果改变目标对象 中引用型字段的值他将反映在原是对象中,也就是说原始对象中对应的字段也会发生变化。
深拷贝与浅拷贝不同的是对于引用的处理,深拷贝将会在新对象中创建一 个新的和原是对象中对应字段相同(内容相同)的字段,也就是说这个引用和原是对象的引用是不同的,我们在改变新对象中的这个字段的时候是不会影响到原始对 象中对应字段的内容。
三、浅拷贝与写时拷贝的结合体
如果不想为对象开辟新的内存空间,但是也不想使得程序崩溃,因此在此处我们就需要定义引用计数的指针,他的任务就是指向对象使用的这块空间,如果这块内存一直在使用,因此就不释放他,如果已经是最后一个对象在使用,在由此对其进行释放。
class String
{
public:
String(char* pStr = "")//构造函数
:_pCount(new int(1))//为引用计数开辟空间
{
if (pStr == NULL)//传过来的字符串为空
{
_pStr = new char[1];//此处这样申请的原因就是,为了释放时的安全,防止内存泄漏,为了达到释放空间时的匹配
*_pStr = '\0';//将内容放在_pStr中
}
else
{
_pStr = new char[strlen(pStr) + 1];
strcpy(_pStr, pStr);//此处拷贝,连同“\0”一起拷贝
}
}
String(const String &s)
:_pStr(s._pStr)//字符串空间的共用
, _pCount(s._pCount)
{
++(*_pCount);//如果共用一块空间,则让其引用计数自增
}
String&operator=(const String &s)
{
if (this != &s)
{
if (0 == --(*_pCount))//当前空间只有一个对象在使用s3=s2;赋值方式
{
delete _pStr;
_pStr = NULL;
}
_pStr = s._pStr;//s2=s3;这种赋值方式
_pCount = s._pCount;
++(*_pCount);
}
return *this;
}
~String()//析构函数
{
if (_pStr&&(0 == --(*_pCount)))//当当前空间只有一个对象在使用之时,就将此空间释放
{
delete _pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
int *_pCount;//定义引用计数的空间
};
再利用此种方式之时,或许有的人就会有人有疑问,为什么不将计数定义为普通变量或者静态变量,现在我们就一一对其作出解答。
1、为什么不将其定义为普通变量
定义为普通变量之时,在析构函数之时,只是对其一个对象中的计数进行改变,,从而使得对象一直不能被销毁,从而导致程序的崩溃。
2.既然要实现共享,那为什么不使用静态变量(static)
在使用静态变量之时,虽然实现了对象的共享,但是在又重新创建另一个对象之时,又将计数的值置为初始值,从而使得被共用的内存不能得到适合的释放,从而又变成了简单的浅拷贝了,从而使得程序崩溃。
因此在引用计数之时,才定义一个指针变量,让其为每个对象均创建一个指针,实时监控着内存的变化,从而实现了引用计数的目的。
四、写时拷贝
利用上面引用计数的方法,虽然可以很好的实现利用浅拷贝实现正确的拷贝,但是在维护起两个指针有些困难,有时会忘了释放某个动态开辟的内存,从而使得内存泄漏,因此,我们就想出一个更好的办法,用来实现浅拷贝,从而更加方便了我们的管理。
class String
{
public:
String(char* pStr = "")//构造函数
{
if (pStr == NULL)//传过来的字符串为空
pStr = '\0';//将内容放在_pStr中
_pStr = new char[strlen(pStr) + 1 + 4];//在此处,多开辟4个字节,将引用计数放在空间的起始位置,真正放字符串的空间,则在内存中的起始偏移4个字节
strcpy(_pStr, pStr);
GetReference() = 1;
}
String(const String &s)
:_pStr(s._pStr)//字符串空间的共用
{
++GetReference();
}
String& operator=(const String &s)
{
if (this != &s)
{
Release();//将其旧空间释放
_pStr = s._pStr;
++GetReference();
}
return *this;
}
char &operator[](size_t index)
{
if (GetReference() > 1)//判断是否共用空间
{
String str(_pStr);//为要修改的空间新开辟另一段空间
swap(_pStr, str._pStr);//在交换期间,又为引用计数加为原来的数,所以在这个函数体内,不用对引用计数的改变。
}
return _pStr[index];
}
~String()//析构函数
{
Release();
}
private:
int& GetReference()
{
return *(int*)(_pStr - 4);//找到起始位置,堆上的空间
}
void Release()//释放空间
{
if (_pStr && (0 == --GetReference()))
{
_pStr -= 4;
delete[] _pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};
int main()
{
String s1("hello");
String s2(s1);
String s3(s2);
s1[0] = 'w';
String s4;
s4 = s2;
//s2 = s4;
system("pause");
return 0;
}
虽然用这种方法,极好的实现了浅拷贝,但是在需要改变哪个字符串中的值时,还有些不方便,因此才有了上面的[]运算符的重载,仅仅只改变某个对象中的某个字符,从而不至于形成“牵一发而动全身”的后果。
下面给出测试代码:
int main()
{
String s1("hello");
String s2(s1);
String s3(s2);
String s4;
s1[0]='w';
s4 = s2;
//s2 = s4;
system("pause");
return 0;
}
对于深拷贝与浅拷贝的内容,大概就这么多了,希望我们一起学习,一起进步。
有什么问题,还请指正!!!!谢谢大家的几分钟。