因为Windows的内核对象也运用了引用计数,所以稍作了解并非无用。
引用计数可以让多个对象共享一个数据,而且免除了跟踪控制权的负担,让对象自己管理自己,当再没有被使用时可以自动删除,也算是一种简易的垃圾回收机制。
另一方面,如果有N多个相同的对象:○=○=○=○=...=○=○ 这样的做法是臃肿且无聊的,所以一个好的做法就是让对象可以共享这一个数据。既可以节省内存,又可以提高效率让程序负担更少,不用构造和析构这个值对象的拷贝了。
1 String a, b, c, d, e; 2 a=b=c=d=e="hello";
1 String& String::operator=(const String &rhs) 2 { 3 if (data==&rhs) return *this; //防止自我赋值 4 delete [] data; 5 data = new char[strlen(rhs.data)+1)]; 6 strcpy(data, rhs.data); 7 return *this; 8 }
用图显示的话,即:
当a被赋予了另外的值,a="world"; 这时候不能删除这个Hello,应外仍然存在bcde,4个对象在共享这个数据;另外,当只有1个对象x在用这个Hello,而x已经超过了其生存期,没有其他对象指向这个Hello的时候,我们需要删除这个Hello确保不发生资源泄漏。这也就意味着引入引用计数后,图将改变成这样:
应该是每一个String值对应一个计数数值,而不是String对象对应一个引用计数。接下来,新建一个嵌套类StringValue来保存计数和其跟踪的值。
String.h
1 #include <string> 2 3 class String { 4 public: 5 String(const char *initValue=""); 6 String& String::operator=(const String &rhs); 7 8 private: 9 // StringValue的主要目的是提供一个空间将一个特别的值和共 10 // 享此值的对象的数目联系起来 11 struct StringValue //嵌套类,引用计数 12 { 13 int refCount; //计数数值 14 char *data; 15 StringValue (const char* initValue); 16 ~StringValue(); 17 }; 18 StringValue *value; 19 };
String.cpp
1 #include "String.h" 2 3 String::StringValue::StringValue(const char* initValue) 4 :refCount(1) 5 { 6 data = new char[strlen(initValue)+1]; 7 strcpy(data, initValue); 8 } 9 10 String::StringValue::~StringValue() 11 { 12 delete [] data; 13 } 14 15 String::String(const char *initValue) 16 :value(new StringValue(initValue)) 17 { 18 19 }
而这样做通常会产生一个问题,
String s1("More Effective C++");
String s2("More Effective C++");
将会变成这样的数据结构:
想办法改进一下:
控制副本的简单实现
1 list<string> String::StringValue::independObj; //独立对象 2 String::StringValue::StringValue(const char* initValue) 3 :refCount(1) 4 { 5 typedef list<string>::iterator lsp; 6 lsp p = find(independObj.begin(), independObj.end(), string(initValue)); 7 if (p==independObj.end()||independObj.empty()) 8 {//未找到对象,新建 9 data = new char[strlen(initValue)+1]; 10 strcpy(data, initValue); 11 independObj.push_back(string(data)); 12 } 13 else 14 { 15 // do something... 16 } 17 }
接下来看下String类的拷贝构造函数
String::String(const String& rhs) : value(rhs.value) { ++value->refCount; }
当这样构造2个对象:
String s1("More Effective C++");
String s2(s1);
就会产生这样的数据结构,其代价是非常低廉的,省去了新对象的构造(不必分配新内存和把内容拷贝到这块内存中)和之后的析构(不必释放那块内存),仅仅是使计数+1和拷贝了下指针
拷贝构造函数之后看下析构函数
String::~String() { if (--value->refCount == 0) { delete value; } }
即,当被引用的对象还有其他共享对象时,仅把计数-1;而当没有其他共享对象时,才彻底将引用对象析构掉。接着,是重载赋值操作符,稍微有些复杂
String& String::operator=(const String &rhs) { if (value == rhs.value) //赋值的是其本身 return *this; //什么也不做 if (--value->refCount == 0) //如果只有当前对象在共享那个数据 delete value; //则删除掉,因为即将被赋予新的引用。不是的话,仅将计数-1 value = rhs.value; //赋值操作 ++value->refCount; //计数器+1 return *this; }
const版本的下标操作仅仅是只读的,不会对引用对象做出修改
const char& String::operator[](int index) const //const版本 { //需下标溢出检查 return value->data[index]; }
需要考虑的是非const版本的下标操作,因为C++编译器无法告诉我们非const的operator[]是会被用来读还是写操作。所以我们保守地认为所有的操作都是“写”的。
char& String::operator[](int index) //非const版本 { if (value.refCount>1) //如果引用对象不止一个 { --value.refCount; //计数减一,相当于把这个引用删除了 value = new StringValue(value->data); //重新申请一份新的拷贝 } return value->data[index]; }
这个思想就是:“与其他对象共享的一个值直到写操作时才拥有自己的拷贝”。即,lazy原则的特例。
在大部分情况下都能满足以上的应用,可是唯一情况却颇为棘手,比如
String s1("More Effective C++");
char* p=&s1;
String s2 = s1;
拷贝构造函数让s2和s1共享这个对象,这时候的数据结构为
如果写下这样一句: p[1]='X'; //将同时修改s1和s2的内容!String 的拷贝构造函数无法检测出s1拥有指向StringValue指针的存在。该问题的一个解决方法就是:在每个StringValue中增加一个标志,表示该对象是否可以被共享。在最初是ture状态,而在调用了非const的operator[]之后则设置成false,且之后永远置于false状态。
追加共享标志位的String
1 class String { 2 public: 3 String(const char *initValue=""); 4 String(const String& rhs); 5 String& operator=(const String &rhs); 6 ~String(); 7 const char& operator[](int index) const; 8 char& operator[](int index); 9 10 private: 11 struct StringValue 12 { 13 int refCount; 14 char *data; 15 bool shareable; //追加 16 StringValue (const char* initValue); 17 ~StringValue(); 18 }; 19 StringValue *value; 20 }; 21 22 //-------------------------------------------------------------------- 23 24 String::StringValue::StringValue(const char* initValue) 25 :refCount(1), 26 shareable(true) //追加 27 { 28 29 data = new char[strlen(initValue)+1]; 30 strcpy(data, initValue); 31 independObj.push_back(string(data)); 32 33 } 34 35 String::String(const String& rhs) //追加 36 { 37 if (rhs.value->shareable) 38 { 39 value = rhs.value; 40 ++value.refCount; 41 } 42 else 43 { 44 value = new StringValue(rhs.value->data); 45 } 46 } 47 48 char& String::operator[](int index) 49 { 50 51 if (value.refCount>1) 52 { 53 --value.refCount; 54 value = new StringValue(value->data); 55 } 56 value.shareable = false; //追加 57 return value->data[index]; 58 }
引用计数不仅运用在字符串类上,只要是多个对象共享相同值的类都可以。
构建一个基类(RCObject),任何需要引用计数的类都必须继承自此类。由RCObject类封装引用计数功能。
RCObject.h
RCObject.cpp
1 #include "RCObject.h" 2 3 RCObject::RCObject() : refCount(0), shareable(true) { } 4 RCObject::RCObject(const RCObject& rhs) : refCount(0), shareable(true) { } 5 RCObject& RCObject::operator= (const RCObject& rhs) { return *this; } 6 RCObject::~RCObject() { } 7 void RCObject::addReference() { ++refCount; } 8 void RCObject::removeReference() { if (--refCount==0) delete this; } 9 void RCObject::markUnshareable() { shareable = false; } 10 bool RCObject::isShareable() const { return shareable; } 11 bool RCObject::isShared() const { return refCount>1; }
RCObject类给了我们一个存储引用计数的地方,并提供了成员函数供我们操作引用计数,但调用这些函数的动作还必须被手工加入其它类中。仍然需要在String的拷贝构造函数和赋值运算函数中调用StringValue的addReference和 removeReference函数。这很笨拙。
StringValue *value; 必须操作StringValue对象的refCount字段。是否能够让指针自身检测发生复制拷贝,赋值操作,析构操作此类事件,而对于计数经行修改的操作呢?答案是否定的。代替的方法就是利用智能指针。
|
请发表评论