本節書摘來自異步社群出版社《c++面向對象高效程式設計(第2版)》一書中的第4章,第4.10節,作者: 【美】kayshav dattatri,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
c++面向對象高效程式設計(第2版)
通過以上的讨論可知,tstring類相當易懂和易實作。如果經常使用該類的對象作為函數參數和按值傳回的值,會出現什麼情況?因為tstring類使用了深複制語義,如果tstring
圖4-12
類對象中的字元數目很多,将花費很長的時間來複制字元和删除動态配置設定記憶體。這也意味着,建立對象和銷毀對象的開銷很大。我們設計tstring類的初衷,就是希望客戶在使用字元串的地方,都能使用tstring類對象。但是,如果建立、複制、指派和銷毀這些對象的開銷太大,難免客戶避而遠之。是否有辦法可以優化實作,加快對象的複制速度?
的确,複制tstring類對象時,也要複制對象中的所有字元。但是,這樣做太浪費時間。我們可以嘗試修改實作,使其在建立多個tstring類對象副本時,讓這些副本都共享原始字元串中的字元,并不真正複制它們。我們了解過如何實作這樣的共享。重要的是,當某個副本企圖修改(或甚至銷毀)對象中的字元時,共享機制必須確定該副本(tstring類對象)獲得一份自己的字元副本,而不會影響其他仍然共享字元的對象。例如(為了解以下代碼,見圖4-12)。
class tstring {
public:
// 構造函數
tstring(); // 建立一個空字元串對象
// 建立一個字元串對象,該對象包含指向字元的s指針。
// s所指向的字元串必須以null結尾,通過s複制字元。
tstring(const char* s);
tstring(char achar); // 建立一個包含單個字元achar的字元串
tstring(const tstring& arg); // 複制構造函數
~tstring(); // 析構函數
// 指派操作符
tstring& operator=(const tstring& arg);
// 傳回指向内部資料的指針,小心。
const char* c_str() const { return _rp->_str; }
// 這些方法将修改原始對象,将其他對象的字元附在 *this後。
// 在字元串中改變字元的情況
tstring& tolower(); // 将大寫字元轉換成小寫
tstring& toupper(); // 将小寫字元轉換成大寫
// 其他成員函數未顯示
private:
struct stringrep {
char* _str; // 實際的字元
unsigned _refcount; // 對它引用的數目
unsigned _length; // 字元串中的字元數目
};
stringrep* _rp; // 在tstring中唯一的資料成員
};<code>`</code>
// 其他非成員函數未作改動--此處未顯示
每個tstring類對象都包含指向stringrep對象的指針。在複制tstring類對象時,隻需複制_rp指針,就這麼簡單。實際上,也可以将strginrep設計成一個帶有構造函數和析構函數的真正獨立的類。但是在該例中,不用這樣做。我們需要的隻是一個字元指針和引用計數的占位符。參見圖4-14了解以下代碼:
tstring::tstring()
{
_rp = new stringrep;
_rp->_refcount = 1;
_rp->_length = 0;
_rp->_str = 0;
}
tstring::tstring(const char* s)
_rp->_refcount = 1; // 這是使用stringrep的唯一對象
_rp->_length = strlen(s);
_rp->_str = new char[_rp->_length + 1];
strcpy (_rp->_str, s);
tstring::tstring(char achar)
_rp->_length = 1;
_rp->_str[0] = achar;
_rp->_str[1] = 0;
_rp->_refcount = 1; // 這是使用stringrep的唯一對象
tstring::tstring(const tstring& other)
// 這是最重要的操作之一。
// 我們需要在other中,通過_rp所指向的對象遞增引用計數。它又獲得一個引用。
other._rp->_refcount++;
// 讓它們共享資源
this->_rp = other._rp;
tstring& tstring::operator=(const tstring& other)
if (this == &other)
return * this; // 自我指派
/* 這是另一個重要的操作。我們需要在other中,通過_rp所指向的對象遞增引用計數。
同時,需要通過“this”指向的對象遞減引用計數。 */
other._rp->_refcount++; // 它又獲得一個引用
// 遞減和測試,是否仍然在使用它?
if (--this->_rp->_refcount == 0) {
delete [] this->_rp->_str;
delete this->_rp;
this->_rp = other._rp; // 讓它們共享資源
return * this;
// 這是一個重要的成員函數,需要應用“寫時複制”方案
tstring& tstring::tolower()
char* p;
if (_rp->_refcount > 1) {
// 這是最困難的部分。分離tstring 對象并提供它的stringrep對象。
// 這是“寫時複制”操作。
unsigned len = this->_rp->_length; // 儲存它
p = new char[len + 1];
strcpy(p, this->_rp->_str);
this->_rp->_refcount--; // 因為 *this即将離開記憶體池
this->_rp = new stringrep;
this->_rp->_refcount = 1;
this->_rp->_length = len;
this->_rp->_str = p; // p在前面已建立
// 繼續,并改變字元
p = this->_rp->_str;
if (p != 0) {
while (*p) {
p = tolower(p); ++p;
}
return * this;
tstring& tstring::toupper() // 留給讀者作為練習
return *this;
tstring::~tstring()
if (--_rp->_refcount == 0) {
delete [] _rp->_str;
delete _rp;
}<code>`</code>
// 已省略其他成員函數的實作
下面的代碼用于說明指派操作符如何工作(見圖4-15):
tstring x(“1234abcxyz”);
tstring y(x);
tstring z = x;
z.tolower();<code>`</code>
現在,分析一下tstring類的析構函數。當tstring類對象離開作用域後,如果不再使用_rp所指向的記憶體,必須将其删除。否則,我們隻是減少了引用計數的值,并未清理記憶體就匆忙前進。
線程安全可移植性:
必須記住,在以上讨論的示例中,所有修改_refcount資料成員的地方,都不是多線程安全的操作。在需要多線程安全的情況中,必須保證這樣的遞增和遞減操作是多線程安全的。方法是:使用作業系統特定的同步工具(甚至是在彙編語言例程中);或者,由一個不同的類(将在下一章中介紹)來處理這種針對處理器的操作,而且客戶必須使用這個類。重要的是識别線程安全,如何實作它隻是細節問題。
思考:
在上面的代碼中,很多地方都需要建立、删除和操控stringrep對象。很明顯,這并不是最好的方法。嘗試修改實作,以便stringrep有自己的構造函數、析構函數以及其他函數。這樣,stringrep便可自我管理。另外,完成tstring類的實作。
共享資源是大多數應用程式中十分常見的功能,在需要共享資源(無論是否有“寫時複制”)時,使用引用計數是一種整潔的方案。引用計數促使實作更高效、更簡潔,而且
圖4-15
使應用程式運作得更快。引用計數為客戶分擔了資源管理的負擔,并讓其成為實作的一部分(這是正确的處理方法)。
上面使用的引用計數方案有一些與衆不同的特點。
tsring類對象負責處理stringrep對象。可以把stringrep對象看成主對象(master object),它擁有存儲區和引用計數。實際上,客戶并不知道内部如何完成所有的工作,因為“寫時複制”方案保證了她不會受到任何影響。客戶總會認為自己擁有了tstring類對象副本,其實真正的實作遠比這複雜得多。“寫時複制”這個概念,在禁止高開銷複制、需要更高效複制操作的地方非常有用。
圖4-16
在不适合使用“寫時複制”方案(因為主對象并不允許複制),但卻需要共享的地方,我們将使用無“寫時複制”的引用計數語義。在所有情況中,都必須考慮是否允許客戶修改主對象。我們将在後面的章節中介紹更多相關的示例。
1這頻繁用于作業系統中程序之間的頁面共享。mach微處理器将該原則用于虛拟記憶體系統。unix系統通過調用vfork()也是為了相同的目的。
本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。