C++ String的引用计数写时复制 的实现 《More Effective C++》
Posted 小丑快学习
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ String的引用计数写时复制 的实现 《More Effective C++》相关的知识,希望对你有一定的参考价值。
文章目录
1.引用计数
c++引用计数的可以节省内存,而且同时可以降低构建对象和析构的开销,所谓引用计数简单说来就是对各对象共享一份实体的数据,但是我们需要实现对该数据的引用的对象的记录,这样最后一个对象引用结束后能够安全的删除数据。
用字符串举例,假设我们想要实现字符串的拷贝或者赋值,那么我们想要呈现的客户的是各自独立的字符串。如下:
但是,对于计算机的内部实现而言,这样的方式显然出现了冗余存储的现象,那么我们期待计算机内部是这样实现的。
这样所有的用户拥有的字符串都是同一个,但是,这会出现一个问题,那就是的那个其中一个销毁对象时,其它对象的数据也将不可访问,因此,我们需要对该份数据的引用进行计数,只有最后一个引用对象才能真正的销毁数据。
因此实现是这样的。
2.写时复制
上述实现计数方式虽然就实现了引用计数,能够使得多个相同的字符串共享同一份数据,但是,当其中任何一个对象修改数据时,其它对象所拥有的数据也就都改变,因此,为了避免这种情况的发生,采用写时复制的技巧(copy on write),也就是当某个对象要修改数据时便重新赋值一份进行修改,从而该对象就有一份新的数据,不在和其它对象共享一份数据。
但是如果出现这样的语句时可能就不那么满意了。
String s1 = "Hello";
char *p = &s1[1];
String s2 = s1;
*p = 'x';
指针p并不能被计数器记录,因此,这将导致共享数据被修改。
为了防止上述情况的出现,当有字符串对共享数据有修改倾向时,则把该数据设为不可共享状态,那么上述的语句中,s2将不会和s1共享数据,而是复制一份新的数据。
下面是实现代码,具有详细的注释:
//引用计数实现string
class My_string
public:
My_string(const char* val = ""):value(new ref_count(val))
My_string(const My_string& other)
//数据可以共享则直接引用,否则重新分配
if (other.value->isShareable)
value = other.value;
++(value->count);
else
value = new ref_count(other.value->data);
My_string & operator=(const My_string & other)
//检查自我赋值
if (this->value == other.value)
return *this;
//如果是最后一个引用,则删除数据
if (-- value->count == 0)
delete this->value;
//如果不可以共享,则需要重新分配空间
if (other.value->isShareable)
value = other.value;
++value->count;
else
value = new ref_count(other.value->data);
return *this;
~My_string()
if ( -- value->count == 0 )
delete value;
const char& operator[]( int val) const
return value->data[val];
//可能会对数据进行修改,所以采用写时复制的策略
char & operator[](int val)
if (value->count == 1) //如果目前只有一个引用则不用复制
return value->data[val];
--value->count;
value = new ref_count(value->data);
value->isShareable = false;//不能共享,以防 char * p = &s[i] 这种方式出现
return value->data[val];
private:
//设置为友元函数,以便能访问私有数据
friend ostream& operator<< ( ostream & cout, const My_string& s);
//对数据的封装
struct ref_count
//字符串数据
char* data;
//是否可共享标志,仅在可能写时设置为false
bool isShareable;
//计数器
int count;
//构造函数
ref_count(const char * data_):count(1),isShareable(true)
data = new char[strlen(data_) + 1];
strcpy_s(data , strlen(data_) + 1 , data_);
//析构函数
~ref_count()
delete[] data;
;
//每个字符串拥有一个指向封装数据的指针
ref_count * value;
;
// << 运算符重载
ostream& operator<< (ostream& cout ,const My_string & s)
for (char* p = s.value->data; *p != '\\0'; p++)
cout << *p;
return cout;
3.代理类区别读写操作
上述的字符串对于随机访问操作符[ ]而言,不管是都读或者写都将会把数据设置为不可共享的状态,因而这将导致数据的共享性并不高,因为对于字符串的操作,随即操作使用的很多,因而,我们需要用某种方式使得读和写能有所区别。使用一个内嵌的代理类能够解决这样的问题。如下代码:
class My_string
public:
//构造函数
My_string(const char* val = "") :value(new ref_count(val))
My_string(const My_string& other)
value = other.value;
++value->count;
//代理类,用于实现对读写的判断,或者左值和右值的引用
class CharProxy
public:
CharProxy( My_string & s_, int index_ ):s(s_),index(index_)
CharProxy & operator=(const CharProxy & cp)
set_String_value();
s.value->data[index] = cp.s.value->data[index];
return *this;
CharProxy& operator=(char c)
set_String_value();
s.value->data[index] = c;
return *this;
//装换为char的隐式类型转换
operator char()
return s.value->data[index];
private:
//重复代码提出,减少代码重复
void set_String_value()
if (s.value->isShared())
--s.value->count;//先将原来的计数减一,在复制
s.value = new ref_count(s.value->data);
My_string& s;
int index;
;
//返回值应该是代理类
const CharProxy operator[](int index) const
return CharProxy(const_cast<My_string&>(*this), index);
CharProxy operator[](int index)
return CharProxy(*this, index);
My_string & operator=(const My_string & other)
//检查自我赋值
if (this->value == other.value)
return *this;
//如果是最后一个引用,则删除数据
if (-- value->count == 0)
delete this->value;
value = other.value;
++value->count;
return *this;
~My_string()
if ( -- value->count == 0 )
delete value;
private:
//设置为友元函数,以便能访问私有数据
friend ostream& operator<< ( ostream & cout, const My_string& s);
friend class CharProxy;//代理类需要访问String的成员value
//对数据的封装
struct ref_count
//字符串数据
char* data;
//是否可共享标志,仅在可能写时设置为false
bool isShareable;
//计数器
int count;
//构造函数
ref_count(const char * data_):count(1),isShareable(true)
data = new char[strlen(data_) + 1];
strcpy_s(data , strlen(data_) + 1 , data_);
//析构函数
~ref_count()
delete[] data;
//设为不可共享
void makeUnshareable()
isShareable = false;
//查看是否被共享
bool isShared()
return count > 1;
;
//每个字符串拥有一个指向封装数据的指针
ref_count * value;
;
// << 运算符重载
ostream& operator<< (ostream& cout ,const My_string & s)
for (char* p = s.value->data; *p != '\\0'; p++)
cout << *p;
return cout;
使用CharProxy类来代表char,这样对于My_string的操作符[ ]的返回值将会是一个CharProxy对象,而该对象中有对该字符串的引用,如果执行如下的操作:
My_string s("hello world!");
s[1] = 'c';
则对于s[1]将会返回一个CharProxy的临时对象,而赋值运算符将会调用该临时对象的重载版本,即如下的版本:
CharProxy& operator=(char c)
set_String_value();
s.value->data[index] = c;
return *this;
//重复代码提出,减少代码重复
void set_String_value()
if (s.value->isShared())
--s.value->count;//先将原来的计数减一,在复制
s.value = new ref_count(s.value->data);
只有调用赋值运算符才会去执行写时复制操作(set_String_value中函数实现),因而,如果客户端不进行赋值,则不会执行写时操作,因而,实现对读写的区分。
而另一个重载版本:
CharProxy & operator=(const CharProxy & cp)
set_String_value();
s.value->data[index] = cp.s.value->data[index];
return *this;
这个版本的赋值运算时为了应对这种情况的赋值运算:
s[1] = s1[1];
通过代理类的实现,我们则不再需要设立标志来判定是否能够共享了,也不需要将数据设为不可共享状态,因而My_string的部分成员函数也应该做出相应的修改。
本文代码参考《More Effective C++》条款29、30写出。更多细节和请仔细阅读此书。
以上是关于C++ String的引用计数写时复制 的实现 《More Effective C++》的主要内容,如果未能解决你的问题,请参考以下文章