由深拷贝与浅拷贝引发的引用计数、写时拷贝技术

时间:2022-04-04 22:10:31

一、理解深拷贝和浅拷贝:

#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;
}


s1给s2初始化时,会调用拷贝构造函数,因为没有编写,则会调用默认的拷贝构造函数,拷贝构造函数会按成员赋值,这样s2的指针会指向s1的指针指向的空间;
但析构的时候,会先释放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时,直接对进行更改