引用計數(reference count),允許多個等值對象共享一個實值,此技術的發展有兩個動機.第一,簡化heap object的簿記工作,在程序執行過程中,對象的擁有權可以會轉移,記錄對象的擁有權不是一件簡單的事情,其次,在對象沒有使用者時,需要自動銷燬自己,避免內存泄露;第二,如果多個對象擁有相同的值,那麼將那個值存儲多次往往是件愚蠢的事情,最好的做法是,讓所有等值對象共享一份實值就好,這麼做不僅節省內存,也使得程序速度加快,因爲不再需要構造和析構同值的多餘副本。
在 C++ 簡易string類實現(一)中我們看到,每一個String變量,都擁有一份heap object,即使其內部完全一樣,在某些情況下(僅僅對String變量讀操作,而沒有寫),就會引發上述中的第二個問題,因此,這裏通過引用計數的方式,來解決這個問題.在這裏,爲了節約篇幅,僅僅寫出主要的幾個成員函數.
類聲明:
class String
{
public:
String(const char* str_ = "");
String(const String& str_);
String& operator=(const String& str_);
~String();
public:
const char& operator[](size_t index_) const;
public:
size_t getRefCount() const;
private:
struct StringValue
{
size_t refCount;
char* ptr;
StringValue(const char* str_);
~StringValue();
};
StringValue* _value;
};
引用計數,顧名思義,是用來計數的,這裏,就需要明確是對String本身計數還是對其擁有的資源進行計數,可想而知,計數的應該是後者.在類String中,字符串的值和引用次數之間有耦合(coupling)關係,利用封裝的思想,將兩者封裝成一個類(class),不但存儲引用次數,也存儲他們所追蹤的對象值,這個class命名爲StringValue,並將其作爲String的一個嵌套(nested)類.爲了讓所有的String成員函數(member function)都能夠訪問StringValue,將StringValue聲明爲struct,這與封裝的思想似乎背道而馳,但是,將StringValue用private修飾,使僅能被String的成員函數訪問,而不被任何其它人訪問,這是在訪問的便利性和封裝之間做了一個折衷,畢竟,作爲一個輔助類,其作用範圍有限(String內),不希望追求極致的封裝而將StringValue本身加入過多額外的成員函數.
類實現:
String::String(const char* str_ /* = "" */)
: _value(new StringValue(str_))
{
}
String::String(const String& str_)
: _value(str_._value)
{
++_value->refCount;
}
String& String::operator=(const String& str_)
{
/*
//這樣寫存在問題,例如str2 = str3; str2 = str3;即重複一次,
//如果是這種寫法,那麼會執行後續代碼一次,雖然結果是對的
//但造成了不必要的運行消耗
if (this != &str_)
{
return *this;
}*/
if (_value == str_._value)
{
return *this;
}
std::cout << "operator=" << std::endl;
if (--_value->refCount == 0)
{
delete _value;
}
_value = str_._value;
++_value->refCount;
return *this;
}
String::~String()
{
if (--_value->refCount == 0)
{
delete _value;
}
}
const char& String::operator[](size_t index_) const
{
//爲了簡化代碼,不引入_size變量記錄字符串長度
if (index_ >= strlen(_value->ptr))
{
throw std::out_of_range("String out of range!");
}
return _value->ptr[index_];
}
size_t String::getRefCount() const
{
return _value->refCount;
}
String::StringValue::StringValue(const char* str_)
: refCount(1)
{
ptr = new char[strlen(str_) + 1];
strcpy(ptr, str_);
}
String::StringValue::~StringValue()
{
if (ptr != nullptr)
{
delete[] ptr;
}
}
問題1:
在這裏,僅僅重載了運算符[]的const版本,因爲這份代碼對讀操作是正常的,但涉及寫時就會出現和預期不一致的問題,因爲多個String共享一個字符串,如果其中一個String變量修改字符串,其結果是,所有String的字符串都被修改了.例如,以下代碼(沒有提供運算符non-const重載,實際上無法通過編譯,僅用於解釋上述):
String str1 = "123";
String str2 = str1;
str2[1] = 2;//沒有提供運算符non-const重載,該行實際上無法通過編譯
對str2的修改,str1內容也會發生變化.
String str1 = "123";
std::cout << str1[2]; //讀操作
str1[1] = 2; //寫操作
因爲C++編譯期無法告訴我們operator[]是被用於讀取或寫,出於安全,這裏假設對operator[]的調用都是寫操作,以確保上述代碼中的問題不會出現,non-const的operator[]代碼如下:
char& operator[](size_t index_);
char& String::operator[](size_t index_)
{
if (index_ >= _value->refCount)
{
throw std::out_of_range("String out of range!");
}
//本對象和其他String對象共享同一個實值
if (_value->refCount > 1)
{
_value->refCount--;
_value = new StringValue(_value->ptr);
}
return _value->ptr[index_];
}
上述對non-const的實現思路:和其它對象共享一份實值,直到我們必須對自己所擁有的那一份實值進行寫動作.這個觀念在計算機科學領域中有很長的歷史,特別是在操作系統領域,各進程(process)之間往往允許共享某些內存分頁(memory pages),直到它們打算修改屬於自己的那一分頁.這項技術是如此普及,因而有一個專用名稱:copy-on-write(寫時才複製).這是提升效率的一般化做法(也就是lazy evaluation,緩式評估)中的一劑特效藥.
問題2:
String s1 = "123456";
char* p = &s1[1];
String s2 = s1;
*p = '8';//希望修改s1[1],結果是s1,s2均修改了
如上述代碼,non-const的operator[]在上述代碼前,就會出現問題.
解決該問題的一個思路:爲每一個StringValue對象加上一個標誌(flag)變量,用於指示可否被共享.一開始,我們先豎立此標誌(表示對象可被共享),但只要non-const operator[]作用域對象值身上就將標誌清除.一旦標誌被清除(設爲false),可能永遠不再改變狀態.
需修改的代碼如下:
struct StringValue
{
size_t refCount;
bool shareable; //新增此行
char* ptr;
StringValue(const char* str_);
~StringValue();
};
String::StringValue::StringValue(const char* str_)
: refCount(1),
shareable(true) //新增此行
{
ptr = new char[strlen(str_) + 1];
strcpy(ptr, str_);
}
String::String(const String& str_)
{
if (str_._value->shareable) //加入判斷條件
{
_value = str_._value;
++_value->refCount;
}
else
{
_value = new StringValue(str_._value->ptr);
}
}
char& String::operator[](size_t index_)
{
...
_value->shareable = false; //新增此行
return _value->ptr[index_];
}
上述兩個問題的解決,都是以數據安全爲前提,由此無法完全做到寫時才複製(copy-on-write),其根本原因是,C++編譯期無法告訴我們operator[]被用於讀取或寫(通過代理類(proxy class(代理類))可以解決這個問題).
問題3
看下述代碼:
void printRefCount(const String& s)
{
std::cout << s.getRefCount() << std::endl;
}
int main(){
{
String s1 = "123456";
printRefCount(s1);
String s2 = s1;
char* p = &s2[1];
printRefCount(s1);
printRefCount(s2);
String s3;
s3 = s2;
printRefCount(s3);
}
system("pause");
return 0;
}
輸出:
由於代碼:
char* p = &s2[1];
導致s2不可以被共享,因此複製運算符函數爲:
String& String::operator=(const String& str_)
{
/*
//這樣寫存在問題,例如str2 = str3; str2 = str3;即重複一次,
//如果是這種寫法,那麼會執行後續代碼一次,雖然結果是對的
//但造成了不必要的運行消耗
if (this != &str_)
{
return *this;
}*/
if (_value == str_._value)
{
return *this;
}
std::cout << "operator=" << std::endl;
if (--_value->refCount == 0)
{
delete _value;
}
if (str_.isShareable())
{
_value = str_._value;
++_value->refCount;
}
else
{
_value = new StringValue(str_._value.ptr);
}
return *this;
}
全部代碼:
class String
{
public:
String(const char* str_ = "");
String(const String& str_);
String& operator=(const String& str_);
~String();
public:
const char& operator[](size_t index_) const;
char& operator[](size_t index_);
public:
size_t getRefCount() const;
private:
struct StringValue
{
size_t refCount;
bool shareable;
char* ptr;
StringValue(const char* str_);
~StringValue();
};
StringValue* _value;
};
String::String(const char* str_ /* = "" */)
: _value(new StringValue(str_))
{
}
String::String(const String& str_)
{
if (_value->shareable)
{
_value = str_._value;
++_value->refCount;
}
else
{
_value = new StringValue(str_._value->ptr);
}
}
String& String::operator=(const String& str_)
{
/*
//這樣寫存在問題,例如str2 = str3; str2 = str3;即重複一次,
//如果是這種寫法,那麼會執行後續代碼一次,雖然結果是對的
//但造成了不必要的運行消耗
if (this != &str_)
{
return *this;
}*/
if (_value == str_._value)
{
return *this;
}
std::cout << "operator=" << std::endl;
if (--_value->refCount == 0)
{
delete _value;
}
if (str_.isShareable())
{
_value = str_._value;
++_value->refCount;
}
else
{
_value = new StringValue(str_._value.ptr);
}
return *this;
}
String::~String()
{
if (--_value->refCount == 0)
{
delete _value;
}
}
const char& String::operator[](size_t index_) const
{
//爲了簡化代碼,不引入_size變量記錄字符串長度
if (index_ >= strlen(_value->ptr))
{
throw std::out_of_range("String out of range!");
}
return _value->ptr[index_];
}
char& String::operator[](size_t index_)
{
if (index_ >= strlen(_value->ptr))
{
throw std::out_of_range("String out of range!");
}
//本對象和其他String對象共享同一個實值
if (_value->refCount > 1)
{
_value->refCount--;
_value = new StringValue(_value->ptr);
}
_value->shareable = false;
return _value->ptr[index_];
}
size_t String::getRefCount() const
{
return _value->refCount;
}
String::StringValue::StringValue(const char* str_)
: refCount(1),
shareable(true)
{
ptr = new char[strlen(str_) + 1];
strcpy(ptr, str_);
}
String::StringValue::~StringValue()
{
if (ptr != nullptr)
{
delete[] ptr;
}
}
注:以上內容是閱讀< < more effective C++> > item 30 的總結;