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++》的主要内容,如果未能解决你的问题,请参考以下文章