引用计数--写时拷贝

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Zhang_1218/article/details/78484587

引用计数--写时拷贝

原文章见www.louhang.xin

假设一下场景:

存在类A,其内含有成员变量为指针类型。首先创建对象a1,给a1 new了一块空间进行了初始化。之后创建了对象a2(可写对象),并且用对象a1进行拷贝构造来完成初始化,此时需要显现的写出拷贝构造函数,为a2 new一块新的空间,以防止浅拷贝,a1和a2都指向同一块空间,两者之间相互影响。但是,又定义了对象a3,且a3为只读对象,a3同样也是用a1拷贝构造来完成初始化,那么这个时候调用了拷贝构造函数,为a3也开辟空间。但是,实质上我们此时只需要完成浅拷贝即可。否则便造成了空间的浪费。

最好的效果便是,当进行只读的操作的时候,那么只进行浅拷贝,而如果要进行写操作,那么就进行深拷贝。

而通过 引用计数-写时拷贝,我们便可以达到这种目的。

写时拷贝(Copy-On-Write)技术,就是编程界“懒惰行为”——拖延战术的产物。举个例子,比如我们有个程序要写文件,不断地根据网络传来的数据写,如果每一次fwrite或是fprintf都要进行一个磁盘的I/O操 作的话,都简直就是性能上巨大的损失,因此通常的做法是,每次写文件操作都写在特定大小的一块内存中(磁盘缓存),只有当我们关闭文件时,才写到磁盘上 (这就是为什么如果文件不关闭,所写的东西会丢失的原因)。

写时拷贝与我之前解析的new[](详见博客《C++动态内存管理:new/delete》),具有相同的做法。

其会有一个变量用于保存引用的数量。当第一个类构造时,string的构造函数会根据传入的参数从堆上分配内存,当有其它类需要这块内存时,这个计数为自动累加,当有类析构时,这个计数会减一,直到最后一个类析构时,此时的引用计数为1或是0。此时,程序才会真正的Free这块从堆上分配的内存。

首先来看下代码:

#include<iostream>
#include<windows.h>
using namespace std;
class String
{
public:
	String(char* str = "")
		:_str(new char[strlen(str) + 1])
	{}

	String(const String& str)
		:_str(new char[strlen(str._str) + 1])
	{
		strcpy(_str, str._str);
	}

	~String()
	{
		if (_str)
		{
			delete[] _str;
		}
	}

private:
	char *_str;
};

void test()
{
	String s1 = "hello";
	double start= GetTickCount();
	for (int i = 0; i < 100000; ++i)
	{
		String s2(s1);
	}
	double end = GetTickCount();
	double len = end - start;
	cout << "test: " << len << endl;
}

int main()
{
	test();
	system("pause");
	return 0;
}

程序运行结果:

TIM截图20171107223529.png

扫描二维码关注公众号,回复: 4930390 查看本文章

在上面代码当中,并未对s1的内容进行改变,但是for循环不断调用拷贝构造函数为s2开辟空间,之后又不断调用析构函数对s2进行释放,造成了空间的不停开辟和析构,就导致了程序效率的降低。

下面来看看写时拷贝应用:

#include<iostream>
#include<windows.h>
using namespace std;

class CopyOnWrite{
public:
	//构造函数
	CopyOnWrite(const char* str="")
		:_str(new char[strlen(str)+5]) //多开辟五个字节,前四个字节用来存放计数器,最后一个用来存放结束标志‘\0’
	{
		_str+=4;
		strcpy(_str,str);
		GetReadCount(_str)=1;
	}
	
	//拷贝构造函数
	CopyOnWrite(const CopyOnWrite& s)
		:_str(s._str)
	{
		++GetReadCount(s._str);
	}

	//析构函数
	~CopyOnWrite()
	{
		Release();
	}

	//获取前四个字节,用作引用计数
	inline int& GetReadCount(char* str)
	{
		return *(int*)(str-4);
	}

	//通过源字符串内的计数器,判断其是否需要释放空间
	inline void Release()
	{
		if (GetReadCount(_str)==0)
		{
			delete[] (_str-4);
			_str=NULL;
		}
		else
		{
			--GetReadCount(_str);
		}
	}
private:
	char* _str;
};

void test1()
{
	CopyOnWrite s1 = "hello";
	double start= GetTickCount();//记录此时毫秒数
	for (int i = 0; i < 100000; ++i)
	{
		CopyOnWrite s2(s1);
	}
	double end = GetTickCount();//记录此时毫秒数
	double len = end - start;
	cout << "test: " << len << endl;
}

int main()
{
	test1();
	system("pause");
	return 0;
}

程序运行结果:

TIM截图20171107223529.png

上面的代码中应用写时拷贝技术,可以很明显的观察到,提高了程序的运行效率。

下面来看一下完整的代码段:

#include<iostream>
#include<windows.h>
using namespace std;

class CopyOnWrite{
public:
	//构造函数
	//多开辟五个字节,前四个字节用来存放计数器,最后一个用来存放结束标志‘\0’
	CopyOnWrite(const char* str="")
		:_str(new char[strlen(str)+5]) 
	{
		_str+=4;//指向开辟空间的的首地址指针向前挪动4个字节
		strcpy(_str,str);
		GetReadCount(_str)=1;//令前四个字节内存引用计数 1
	}
	
	//拷贝构造函数
	CopyOnWrite(const CopyOnWrite& s)
		:_str(s._str)
	{
		++GetReadCount(s._str);//引用计数加1
	}

	//赋值运算符的重载
	CopyOnWrite operator=(CopyOnWrite& str)
	{
		if (this != &str)
		{
			Release();
			_str=str._str;
			str.GetReadCount(_str)++;
		}
		return *this;
	}

	// []重载
	char& operator[](size_t pos)
	{
		if (GetReadCount(_str)>1)
		{
			--GetReadCount(_str);
			//进行写操作的时候,new一块新空间
			char* tmp = new char[strlen(_str)+1];
			tmp+=4;
			GetReadCount(tmp)=1;
			strcpy(tmp,_str);
			_str=tmp;
		}
		return _str[pos];
	}
	
	//打印函数
	void prit()
	{
		cout<<_str<<endl;
	}
	
	//析构函数
	~CopyOnWrite()
	{
		Release();
	}

	//获取前四个字节,用作引用计数
	inline int& GetReadCount(char* str)
	{
		return *(int*)(str-4);
	}

	//通过源字符串内的计数器,判断其是否需要释放空间
	inline void Release()
	{
		if (GetReadCount(_str)==0)//引用计数为0,则要进行空间的释放
		{
			delete[] (_str-4);
			_str=NULL;
		}
		else//引用计数不为0,则将引用计数减1
		{
			--GetReadCount(_str);
		}
	}
private:
	char* _str;
};

void test1()
{
	CopyOnWrite s1 = "hello";
	double start= GetTickCount();//记录此时毫秒数
	for (int i = 0; i < 100000; ++i)
	{
		CopyOnWrite s2(s1);
	}
	double end = GetTickCount();//记录此时毫秒数
	double len = end - start;
	cout << "test: " << len << endl;
}

void test2()
{
	CopyOnWrite s1("hello");
	CopyOnWrite s2(s1);
	CopyOnWrite s3;
	s3 = s2;
	s1[2] = 'x';
	s1.prit();
	s2.prit();
	s3.prit();
}
int main()
{
	test1();
	test2();
	system("pause");
	return 0;
}

下面来对代码进行分析:

TIM截图20171107223529.png

上面的图便是对对象内部的实现进行的分析,我们可以观察到在拷贝一个对象的时候,并没有开辟新的空间,然后把原先的对象复制到新内存,而是在新对象也指向源对象的位置,并把源对象内存的引用计数进行加加。

在对新的对象执行读操作的时候,内存数据不发生任何变动,直接执行读操作;而在对新的对象执行写操作时,将真正的开辟新的空间,将源对象复制到新的内存地址中,并修改新对象的内存映射表指向这个新的位置,并在新的内存位置上执行写操作。

因此在执行复制操作时因为不是真正的内存复制,而只是建立了一个指针,因而大大提高效率。

那么看看下面的三种方法能不能实现写时拷贝呢?

image.png

我们可以分析一下:

方案一:通过成员变量 _refCount来进行引用计数,但是_refCount是每个成员内部的各自的成员变量,其无法记录多个对象的统一情况,所以此方案不行。

方案二:通过静态成员变量 _refCount来进行引用计数,因为其是静态的,所以属于所有对象,这么看来其好像可以实现引用计数,但是,需要注意的是,当存在两个或两个以上的对象动态空间时,如果要对一个进行释放,那么势必要将_refCount置0,但是当_refCount为0时,那么势必会影响到另一个不想进行释放的对象,此时就出现了bug。所以,此方案不通过。

方案三:通过一个指针变量来进行引用计数,其实现方式上面的示例代码中异曲同工之妙,不过上述示例的代码时此方法的优化,此方法可行!

总结:

写时拷贝技术需要跟虚拟内存和分页同时使用,好处就是在执行复制操作时因为不是真正的内存复制,而只是建立了一个指针,因而大大提高效率。

但这不是一直成立的,如果在复制新对象之后,大部分对象都还需要继续进行写操作会产生大量的分页错误,得不偿失。

所以高效的情况只是在复制新对象之后,在一小部分的内存分页上进行写操作。


猜你喜欢

转载自blog.csdn.net/Zhang_1218/article/details/78484587
今日推荐