一、理解深拷贝和浅拷贝:
#include <iostream>
using namespace std;
class String
{
public:
String(const char *str = "")
{
if(str == NULL)
{
data = new char[1];
data[0] = '\0';
}
else
{
data = new char[strlen(str)+1];
strcpy(data,str);
}
}
~String()
{
delete []data;
data = NULL;
}
private:
char *data;
};
int main()
{
String s1("hello");
String s2 = s1;
String s3;
s3 = s1;
return 0;
}
但析构的时候,会先释放s2指向的空间,但当析构s1指向的空间时,因为s2和s1是指向相同空间的,s2已经将空间释放,s1就没有空间可以释放,所以s1的析构就导致了程序的非法访问,造成程序的崩溃。这种现象就叫做浅拷贝,即只拷贝指针。
//重写拷贝构造函数:
String(const String &s) //深拷贝
{
data = new char [strlen(s.data)+1];
strcpy(data,s.data);
}
//重写赋值语句:
String& operator=(const String &s) //深赋值
{
if(this != &s)
{
delete []data;
data = new char[strlen(s.data)+1];
strcpy(data,s.data)
}
return *this;
}
深拷贝就是在拷贝的时候,将指针指向的空间也一同拷贝,这样,析构的时候,自己释放自己指向的空间就可以了。
二、理解深拷贝和浅拷贝各自的优缺点:
浅拷贝节省空间,相同的数据只保存一份,但因为多个指针指向同一个空间,会引发多次释放的问题;
深拷贝虽然每个指针会指向不同的空间,没有一个空间多次释放的问题,但可能保存的数据都是一样的,这样会导致空间的浪费。
三、使用引用计数解决浅拷贝实现中出现的问题:
所以只要能够解决浅拷贝中的同一个空间多次的释放的问题,当然是最好的!
这就引出了引用计数的方法:
当一个空间被一个指针指向时,计数为1,当每多一个指针指向时,计数加 1.
当析构时,释放一个指针对象,空间不释放,计数减 1,当计数为 0 时,释放空间
#include <iostream>
using namespace std;
class String
{
public:
String(const char *str = "")
{
if(str == NULL)
{
data = new char[1];
data[0] = '\0';
}
else
{
data = new char[strlen(str)+1];
strcpy(data,str);
}
++use_count;
}
//重写拷贝构造函数:
String(const String &s) //浅拷贝,引用计数加 1
{
data = s.data;
++use_count;
}
//重写赋值语句:
String& operator=(const String &s) //浅赋值,引用计数加 1
{
if(this != &s)
{
data = s.data;
++use_count;
}
return *this;
}
~String() //析构,引用计数减 1
{
if(--use_count == 0) //当引用计数为 0 时,释放空间
{
delete []data;
data = NULL;
}
}
private:
char *data;
static int use_count;
};
int String::use_count = 0;
int main()
{
String s1("hello");
String s2 = s1;
return 0;
}
运行上面的程序看着没有问题,可是,当我们再创建一个不同的对象时发现,不同的空间居然有相同的引用计数
String s3("world");
s3没有拷贝s1和s2,而是一个新的空间的指针对象,但我们发现还是相同的引用计数加 1,所以这样写的引用计数程序是有问题的。
注意:每个空间应该具有自己的引用计数,而不能所有空间共享一个引用计数。
四、解决引用计数中的写时拷贝技术实现
//引用计数器类
class String_rep
{
public:
String_rep(const char *str):use_count(0)
{
if(str == NULL)
{
data = new char[1];
data[0] = '\0';
}
else
{
data = new char[strlen(str)+1];
strcpy(data,str);
}
}
String_rep(const String_rep &rep):use_count(0)
{
data = new char[strlen(rep.data)+1];
strcpy(data,rep.data);
}
String_rep& operatro=(const String_rep &rep)
{
if(this != &rep)
{
delete []data;
data = new char[strlen(rep.data)+1];
strcpy(data,rep.data);
}
return *this;
}
~String_rep()
{
delete []data;
data = NULL;
}
public:
void increment()
{
++use_count;
}
void decrement()
{
if(--use_count == 0)
{
delete this; //调动自身的析构函数
}
}
private:
char *data;
int use_count;
};
class String
{
public:
String(const char *str = "")
{
rep = new String_rep(str);
rep->increment();
}
String(const String &s)
{
rep = s.rep;
rep->increment();
}
~String()
{
rep->decrement();
}
private:
Stirng_rep *rep;
};
int main()
{
String s1("hello");
String s2 = s1;
String s3("world");
return 0;
}
一个String对象中只维护一个 指向String_rep类的rep指针:
s1 String_rep
[rep] ----- > data -------->[ h e l l o \0]
| use_count
| /
s2 /
[rep]_____/
创建s1对象,调用构造函数,指向一个String_rep对象,引用计数加 1
s1给s2初始化,调用拷贝构造函数,进行浅拷贝,s1和s2指向相同的String_rep对象,引用计数加 1,该对象的指针指向同一个空间
s3 String_rep
[rep]-------> data -------->[ w o r l d \0]
use_count
创建s3对象,调用构造函数,指向一个新的String_rep对象,引用计数加 1
赋值语句:
s3 = s2:
String& operator=(const String &s) //赋值函数的编写要小心,只进行浅拷贝会发生内存泄漏
{
if(this != &s)
{
rep = s.rep;
rep->increment();
}
return *this;
}
赋值函数的编写要小心,只进行浅拷贝会发生内存泄漏,因为s3对象的rep指针原本指向的是String_rep对象,及String_rep
对象指针指向的空间,如果单纯将s2对象的rep值赋值给s3对象的rep值,则s3对象的rep指针指向的空间内存都会泄漏;
重写赋值语句:
String& operator=(const String &s)
{
if(this != &s)
{
rep->cecrement(); //delete
rep = s.rep; //new
rep->increment(); //strcpy
}
return *this;
}
将s3对象rep指针原先指向String_rep的引用计数减 1,再将s3的rep指针赋值为s2的rep指针,该String_rep对象的引用计数 加1
以上的浅拷贝的引用计数方式,解决了相同数据多份空间而造成浪费的问题,但如果我们更改任何一个空间的内容时,所有的拷贝都会发生更改,这是错误的,应该只更改自己的,不应该影响别的对象。
这就提出了写时拷贝技术,即只是拷贝时共享相同的空间,但当自己需要修改数据时,应该将数据拷贝出来,
然后改变自己的指向,即进行深拷贝。
//当需要修改时,在String类中的修改函数:
s2.to_upper();
void to_upper()
{
if(rep->use_count > 1)
{
String_rep *new_rep = new String_rep(rep->data); //1.
rep->decrement(); //2.
rep = new_rep; //3.
rep->increment();
}
char *ch = rep->data; //4.
while(*ch != '\0')
{
*ch -= 32;
++ch;
}
}
当s2对象的rep指针指向的String_rep引用计数大于1时,修改时
1.用原来String_rep对象指针指向的数据创建一个新的String_rep对象;
2.将s2对象的rep指针指向的String_rep引用计数减 1;
3.将s2对象的rep指针指向新的String_rep对象,并将引用计数加 1
4.对s2对象的rep指针指向的新的String_rep对象指针指向的数据进行更改。
当s2对象的rep指针指向的String_rep引用计数等于1时,直接对进行更改