C++写时拷贝技术与引用计数

今天来谈谈c++string类的写时拷贝技术和引用计数。

我们先看上次分享的String类(http://blog.csdn.net/auzeonfung/article/details/78693905)程序的一部分。

String.h

#pragma once
#include 
using namespace std;
class String
{
public:
	String(char* str = "");
	String(const String& s);
	String& operator=(String s);
	~String();
	void String::Show();
	void Expand(size_t n);
private:
	char* _str;
	size_t _size; // 字符个数 
	size_t _capacity; // 容量空间 
};
String.cpp

#include"String.h"

String::String(char* str = "")
	: _size(strlen(str))
	, _capacity(strlen(str) + 1)
	, _str(new char[strlen(str) + 1])
{
	strcpy(_str, str);
}

String::String(const String& s)
	: _capacity(s._capacity)
	, _size(s._size)
	, _str(NULL)
{
	String tmp(s._str);
	swap(_str, tmp._str);
}

String& String::operator=(String s)
{
	if (_str != s._str)
	{
		String tmp(s._str);
		_size = tmp._size;
		_capacity = tmp._capacity;
		swap(_str, tmp._str);
	}
	return *this;
}

String::~String()
{
	if (_str != NULL)
	{
		delete[] _str;
		_str = NULL;
		_size = 0;
		_capacity = 0;
	}
}

void String::Show()
{
	size_t i = 0;
	while (i < _size)
	{
		cout << 
		*(_str + i);i++;
	}
	cout << endl;}

void String::Expand(size_t n)
{
	size_t newCapacity = (_capacity * 2 > _capacity + n) ? (_capacity * 2) : (_capacity + n);
	char* tmp = new char[newCapacity];
	trcpy(tmp, _str);
	delete[] _str;
	_str = tmp;
	_capacity = newCapacity;
}

首先测试使用构造函数、拷贝构造创建两个String类的对象

void test()
{
	String s1("abcdefg");
	String s2(s1);
}
运行后
C++写时拷贝技术与引用计数_第1张图片

显然这两个对象占用的是两块不同的空间。

但是,实际上很多时候我们使用拷贝构造和重载运算符“=”的时候,不会去修改当中的值,当我使用拷贝构造或者重载“=”号对多个对象赋予相同的值的时候,就要进行多次的写入操作,未免有点繁琐,所以我们现在介绍一种技术,叫做“写时拷贝(Copy On Write)”。

写时拷贝,顾名思义就是在写的时候才进行拷贝,而在进行拷贝构造或者“=”赋值的时候,直接让对象的指针指向被拷贝空间,这样可以省下多次对内存的写入,并且可以剩下很多空间。当一个对象不需要修改的时候,直接使用原来的空间直到对象销毁,若它需要修改,则在修改的时候把它拷贝到另外一个空间里面,把对象指针指向该空间,再进行修改操作。

好的,现在我们来试着修改程序中的拷贝构造与“=”的重载:

String::String(const String& s)
	: _capacity(s._capacity)
	, _size(s._size)
	, _str(s._str)//直接把原来对象s的指针的值赋给新对象
{}

String& String::operator=(String s)
{
	if (_str != s._str)
	{
		_size = s._size;
		_capacity = s._capacity;
		_str = s._str;//直接把原来对象s的指针的值赋给当前对象
	}
	return *this;
}
我们想在来测试一下:
void test()
{
	String s1("abcdefg");
	String s2(s1);
	String s3 = s1;
}

现在三个对象都共用了一块空间了。

现在我们补充一个CopyOnWrite函数,并加在Pushback函数里,实现写时拷贝:

void String::CopyOnWrite()
{
	String tmp(_str);//创建一个对象tmp
	swap(_str, tmp._str);//让tmp和当前对象的空间调换
}

void String::PushBack(char ch)
{
	CopyOnWrite();//先复制,再插入
	String tmp(_str);
	tmp.CheckCapacity(1);
	*(tmp._str + tmp._size++) = ch;
	*(tmp._str + tmp._size) = '\0';
	swap(tmp._str, _str);
}

 
  但是问题来了: 
  

测试函数:

void test()
{
	String s1("abcdefg");
	String s2(s1);
	String s3 = s1;
	s2.PushBack('s');
}

s2的确是被拷贝到另外的地方进行写操作了,但是原来那块空间的字符串却无效了。

我们认真看看析构函数:

String::~String()
{
	if (_str != NULL)
	{
		delete[] _str;
		_str = NULL;
		_size = 0;
		_capacity = 0;
	}
}
我们在CopyOnWrite()函数里面创建了一个tmp临时对象,隐式开辟了一块空间,然后把当前对象的字符串指针跟临时对象的字符串指针进行了交换,当函数结束的时候,tmp对象自动调用析构函数进行销毁,把临时对象tmp的字符串指针指向的空间用delete[]操作符销毁了。而临时对象tmp的字符串指针指向的空间则是跟s1、s3对象的字符串指针指向的空间是同一块空间,也就是说在销毁tmp对象进行delete[] _str的时候,删除的亦是s1和s3对象的字符串。

为了防止在删除临时对象的时候同时删除原来对象,我们会想到一下方案:

1.重写析构函数,不要让delete[]操作进行。

2.在CopyOnWrite()函数中不要采用交换(swap()函数)的方式,而是让tmp指向另外一块空间,防止delete[]操作对原来空间进行销毁。

但是这两个方法显然不合理:

1.不进行delete[]操作的话,在没有对象的字符串指针指向那块空间的时候,那块内存得不到释放,会有严重的内存泄漏问题!

2.让tmp指向另一块空间的时候,当tmp对象析构函数执行的时候delete[]操作回去销毁一块没有内容的空间,导致程序崩溃。

因此,只能在一定的情况下让delete[]函数执行,所谓的“一定情况”则是当原来指向这块空间的所有对象都指向另一块空间或者全部被销毁的时候,因此我们引入一个概念:引用计数

所谓引用计数,即是当一块空间被指向的时候,自带一个计数器自增,当指向这块空间的指针不再指向它时,自带的计数器自减。当这个计数器的数值为0的时候,执行析构函数才会执行delete[] _str 这条语句。

而对于这个引用计数,我们有三下方案:

1.增加类内定义:

private:

size_t _refCount;

2.增加类内定义:

private:

static size_t _refCount;

3.增加类内定义:

private:

size_t* _refCount;


以上三种方案只有第三种可行:

第一种,创建对象时是每个对象创建一个_refCount,显然不行;

第二种,使用静态量,但是会出现一种情况:当创建s1这个对象的时候,引用计数自增,再创建一个与s1无关的对象s2的时候,引用计数也会自增,但我们理想的情况却不是如此,仅仅是想要在以s1为原型进行拷贝构造或者“=”赋值的时候才让引用计数自增。

因此采用第三种方案,修改程序(红字为修改内容)

String.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include 
using namespace std;

class String
{
public:
	String(char* str = "");
	String(const String& s);
	String& operator=(String s);
	~String();
	void String::Show();
	void Expand(size_t n);
	void CheckCapacity(size_t num);
	void PushBack(char ch);
	void CopyOnWrite();
private:
	char* _str;
	size_t _size; // 字符个数 
	size_t _capacity; // 容量空间 
	size_t* _refCount; //增加引用计数
};
String.c

#include "String.h"

String::String(char* str)
	: _size(strlen(str))
	, _capacity(strlen(str) + 1)
	, _str(new char[strlen(str) + 1])
	, _refCount(new size_t(1))
{
	strcpy(_str, str);
}

String::String(const String& s)
	: _capacity(s._capacity)
	, _size(s._size)
	, _str(s._str)
	, _refCount(s._refCount)
{
	(*_refCount)++;
}

String& String::operator=(String s)
{
	if (_str != s._str)
	{
		if (--(*_refCount) == 0)
		{
			delete[] _str;
			delete _refCount;
		}
		_size = s._size;
		_capacity = s._capacity;
		_str = s._str;
		_refCount = s._refCount;
		(*_refCount)++;
	}
	return *this;
}

String::~String()
{
	if (_str != NULL)
	{
		if (--(*_refCount) == 0)
		{
			delete _refCount;
			delete[] _str;
		}
		_str = NULL;
		_size = 0;
		_capacity = 0;
	}
}

void String::Show()
{
	size_t i = 0;
	while (i < _size)
	{
		cout << *(_str + i);
		i++;
	}
	cout << endl;
}

void String::Expand(size_t n)
{
	size_t newCapacity = (_capacity * 2 > _capacity + n) ? (_capacity * 2) : (_capacity + n);
	char* tmp = new char[newCapacity];
	strcpy(tmp, _str);
	delete[] _str;
	_str = tmp;
	_capacity = newCapacity;
}

void String::CheckCapacity(size_t num)
{
	if (_size + num >= _capacity)
	{
		Expand(num);
	}
}

void String::CopyOnWrite()
{
	String tmp(_str);
	swap(_str, tmp._str);
	swap(_refCount, tmp._refCount);
}

void String::PushBack(char ch)
{
	if ((*_refCount) != 1)
	{
		CopyOnWrite();
	}
	String tmp(_str);
	tmp.CheckCapacity(1);
	*(tmp._str + tmp._size++) = ch;
	*(tmp._str + tmp._size) = '\0';
	swap(tmp._str, _str);
	_size++;
}

再进行测试,在内存里查看_refCount就可以了。

你可能感兴趣的:(程序分享)