天天看點

《C++面向對象高效程式設計(第2版)》——4.10 “寫時複制”的概念include include include include

本節書摘來自異步社群出版社《c++面向對象高效程式設計(第2版)》一書中的第4章,第4.10節,作者: 【美】kayshav dattatri,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

c++面向對象高效程式設計(第2版)

通過以上的讨論可知,tstring類相當易懂和易實作。如果經常使用該類的對象作為函數參數和按值傳回的值,會出現什麼情況?因為tstring類使用了深複制語義,如果tstring

《C++面向對象高效程式設計(第2版)》——4.10 “寫時複制”的概念include include include include

圖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-&gt;_refcount = 1;

 _rp-&gt;_length = 0;

 _rp-&gt;_str = 0;

}

tstring::tstring(const char* s)

 _rp-&gt;_refcount = 1;   // 這是使用stringrep的唯一對象

 _rp-&gt;_length = strlen(s);

 _rp-&gt;_str = new char[_rp-&gt;_length + 1];

 strcpy (_rp-&gt;_str, s);

tstring::tstring(char achar)

 _rp-&gt;_length = 1;

 _rp-&gt;_str[0] = achar;

 _rp-&gt;_str[1] = 0;

 _rp-&gt;_refcount = 1;  // 這是使用stringrep的唯一對象

tstring::tstring(const tstring&amp; other)

// 這是最重要的操作之一。

// 我們需要在other中,通過_rp所指向的對象遞增引用計數。它又獲得一個引用。

 other._rp-&gt;_refcount++;

  // 讓它們共享資源

 this-&gt;_rp = other._rp;

tstring&amp; tstring::operator=(const tstring&amp; other)

 if (this == &amp;other)

  return * this;  // 自我指派

/* 這是另一個重要的操作。我們需要在other中,通過_rp所指向的對象遞增引用計數。

同時,需要通過“this”指向的對象遞減引用計數。 */

other._rp-&gt;_refcount++;  // 它又獲得一個引用

// 遞減和測試,是否仍然在使用它?

if (--this-&gt;_rp-&gt;_refcount == 0) {

  delete [] this-&gt;_rp-&gt;_str;

  delete this-&gt;_rp;

this-&gt;_rp = other._rp; // 讓它們共享資源

return * this;

// 這是一個重要的成員函數,需要應用“寫時複制”方案

tstring&amp; tstring::tolower()

 char* p;

 if (_rp-&gt;_refcount &gt; 1) {

  // 這是最困難的部分。分離tstring 對象并提供它的stringrep對象。

  // 這是“寫時複制”操作。

  unsigned len = this-&gt;_rp-&gt;_length; // 儲存它

  p = new char[len + 1];

  strcpy(p, this-&gt;_rp-&gt;_str);

  this-&gt;_rp-&gt;_refcount--; // 因為 *this即将離開記憶體池

  this-&gt;_rp = new stringrep;

  this-&gt;_rp-&gt;_refcount = 1;

  this-&gt;_rp-&gt;_length = len;

  this-&gt;_rp-&gt;_str = p;  // p在前面已建立

// 繼續,并改變字元

p = this-&gt;_rp-&gt;_str;

if (p != 0) {

  while (*p) {

   p = tolower(p); ++p;

  }

 return * this;

tstring&amp; tstring::toupper() // 留給讀者作為練習

 return *this;

tstring::~tstring()

 if (--_rp-&gt;_refcount == 0) {

   delete [] _rp-&gt;_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類的實作。

共享資源是大多數應用程式中十分常見的功能,在需要共享資源(無論是否有“寫時複制”)時,使用引用計數是一種整潔的方案。引用計數促使實作更高效、更簡潔,而且

《C++面向對象高效程式設計(第2版)》——4.10 “寫時複制”的概念include include include include

圖4-15

使應用程式運作得更快。引用計數為客戶分擔了資源管理的負擔,并讓其成為實作的一部分(這是正确的處理方法)。

上面使用的引用計數方案有一些與衆不同的特點。

tsring類對象負責處理stringrep對象。可以把stringrep對象看成主對象(master object),它擁有存儲區和引用計數。實際上,客戶并不知道内部如何完成所有的工作,因為“寫時複制”方案保證了她不會受到任何影響。客戶總會認為自己擁有了tstring類對象副本,其實真正的實作遠比這複雜得多。“寫時複制”這個概念,在禁止高開銷複制、需要更高效複制操作的地方非常有用。

《C++面向對象高效程式設計(第2版)》——4.10 “寫時複制”的概念include include include include

圖4-16

在不适合使用“寫時複制”方案(因為主對象并不允許複制),但卻需要共享的地方,我們将使用無“寫時複制”的引用計數語義。在所有情況中,都必須考慮是否允許客戶修改主對象。我們将在後面的章節中介紹更多相關的示例。

1這頻繁用于作業系統中程序之間的頁面共享。mach微處理器将該原則用于虛拟記憶體系統。unix系統通過調用vfork()也是為了相同的目的。

本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。

繼續閱讀