天天看點

《C++面向對象高效程式設計(第2版)》——4.8 為什麼需要副本控制

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

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

在讨論了對象的複制和指派後,現在來學習為什麼需要副本控制。你可能形成這樣的一種觀點,即每個類都應該提供public複制構造函數和指派操作符函數。

但是,實際并非如此。很多情況都存在禁止複制對象的語義;另外某些情況下,複制可能僅對一組標明的客戶有意義;甚至還有些情況,隻允許限定數量的對象副本。所有這些情況都要求有正确且高效的副本控制。在接下來的内容中,我們将舉例說明副本控制的必要性。一旦了解這些示例,你将體會到,c++基于每個類提供的副本控制機制如此地靈活。控制建立對象和複制對象的一般技巧将在後面章節中介紹。

假設有一個tsemaphore類。信号量(semaphore)用于過程(或線程)間的同步,以確定安全共享資源。當一個過程需要使用一個共享資源時,該過程需要靠獲得守護資源的信号量來確定互斥。這可以通過tsemaphore類提供的acquire方法完成。如果所需資源已被其他任務獲得,則acquire調用發生阻塞,而且調用的任務将等待,直到其他任務放棄(relinquish)資源。有可能出現多個任務同時等待相同資源的情況。

一旦任務獲得資源,它便完全擁有該資源的所有權,直至通過調用release成員函數放棄資源。鑒于此,複制信号量對象是否正确?更确切地說,複制信号量對象的語義是什麼?如果允許複制,那麼是否意味着有兩個都已獲得相同資源的獨立信号量對象?這在邏輯上不正确,因為任何時候一個程序隻能獲得一個資源(或在已統計信号量的情況下有限數量的程序)。或者,這意味着兩個信号量對象共享相同的資源?共享狀态可能是一個較好的解決方案,但是,這使得信号量的實作和使用複雜化。信号量被看做是經常使用的“輕量級”對象,使其實作複雜化并不合理。在支援任何複制操作之前,還需要澄清一個問題:也許更好的解決方案應該是禁止任何複制。這意味着一旦建立信号量,任何人都不能複制它。

以下是tsemaphore類的接口。

class tautosemaphore {

    public:

      tautosemaphore(tsemaphore& sem)

        : _semaphore(sem)

      { _semaphore.acquire(); }

      ~tautosemaphore() { _semaphore.release(); }

    private:

      tsemaphore& _semaphore;

};

利用這個類,f()中的代碼可以簡化為:

void x::f() // x的成員函數

{

   // 建立tautosemaphore類對象,同時也獲得信号量。

   tautosemaphore autosem(_sem);

   // 希望完成的任務

   if (/ 某些條件 /) { / 一些代碼 / return; }

   else { / 其他代碼 / }

   // autosem的析構函數在退出f()時,自動釋放_sem信号量  

}<code>`</code>

tautosemaphore類的構造函數期望傳入一個信号量對象,并将信号量作為構造函數的一部分。tautosemaphore類的析構函數負責釋放所獲得的信号量。是以,一旦在某作用域内建立了tautosemaphore類的對象,它的析構函數将會確定釋放已獲得的信号量,程式員無需為此擔心。至少現在看來,需要我們管理的事務又少了一件。

這樣的類在c++程式中非常普遍。另一個類似的類是ttracer,它用于跟蹤進入函數和從函數退出。

class tlicensetoken; // 前置聲明

class tlicenseserver {

 public:

  // 構造函數 – 建立一個有maxusers個許可證的新許可證服務

  tlicenseserver(unsigned maxusers);

  ~tlicenseserver();

  // 授予新許可證或傳回0。主調函數采用已傳回的對象。

  // 不再使用令牌時,應将其銷毀 – 見下文

  tlicensetoken* createnewlicense();

 private:

  // 對象不能被複制或指派

  tlicenseserver(const tlicenseserver&amp; other);

  tlicenseserver&amp; operator=(const tlicenseserver&amp; other);

  unsigned _numissued;

  unsigned _maxtokens;

  // 省略若幹細節

class tlicensetoken {

  public:

   tlicensetoken();

   ~tlicensetoken();

  tlicensetoken(const tlicensetoken&amp; other);

  tlicensetoken&amp; operator=(const tlicensetoken&amp; other);

};<code>`</code>

既然tlicensetoken是由tlicenseserver以使用者為機關而發出的,那麼確定使用者無法複制傳回的令牌非常重要。否則,許可證伺服器将無法控制使用者的數量。每當新使用者希望使用由許可證伺服器控制的應用程式時,他請求tlicenseserver生成一個新的tlicensetoken類對象。如果可以生成新令牌,則傳回一個指向新tlicensetoken的指針。該令牌由調用者所擁有,使用者不再需要使用應用程式時,必須銷毀它。當許可證令牌被銷毀時,它将與許可證伺服器通信,以減少未歸還許可證令牌數目。注意,許可證伺服器和令牌都不能被複制,使用者不可以複制令牌。許可證令牌可包含許多資訊,如任務辨別号、機器名、使用者名、産生令牌的日期等。因為許可證伺服器和令牌的複制構造函數和指派操作符都為私有,是以不可能複制令牌,這便消除了使用欺騙手段的可能性。

要求使用者銷毀令牌是件麻煩事。我們可以完成這樣的實作,即在令牌追蹤軟體使用的同時,如果軟體在預定時間内未被使用,該實作保證能自動地銷毀許可證令牌。實際上,這樣的實作十分常見。

賬單管理是該實作的一個應用,可根據客戶所使用的服務來收費。這廣泛應用于有線電視的按次計費的程式中2。

你可能覺得不允許複制令牌的限制過于嚴格。但是,如果允許這樣做應該考慮建立一個新令牌,并通知許可證伺服器進行複制。可以完成這樣的實作,這仍然需要副本控制。

4.8.3 字元串類示例

各種語言的程式員都使用字元串來顯示錯誤消息、使用者提示等,我們也經常使用和操控這樣的字元串數組。字元串數組的主要問題是存儲區管理和缺少可以操控它們的操作。在c和c++中,字元串數組不能按值傳遞,隻能傳遞指向數組中第1個字元的指針。這很難實作安全數組。為克服這個障礙,我們應該實作一個tstring類提供所有必須的功能。tstring類對象管理自己的記憶體,而且它會在需要時配置設定更多的記憶體,我們無需為此擔心。

注意:

c++标準庫包含一個功能強大的string類,也用于處理多位元組字元。由于string類易于了解,同時能清楚地說明概念,是以在下面的示例中将用到它。

以下是類tstring的聲明:

tstring::tstring()

  _str = 0;

  _length = 0;

}

tstring::tstring(const char* arg)

  if (arg &amp;&amp; *arg) {  // 指針不為0,且指向有效字元。

    _length = strlen(arg);

    _str = new char[_length + 1];

    strcpy(_str, arg);

  }

  else {

   _str = 0;

   _length = 0;

tstring::tstring(char achar)

  if (achar) {

    _str = new char[2];

    _str[0] = achar;

    _str[1] = ‘0’;

    _length = 1;

    _str = 0; _length = 0;

 }

tstring::~tstring() { if (_str != 0) delete [] _str; }

// 複制構造函數,執行深複制。為字元配置設定記憶體,然後将其複制給this。

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

  if (arg._str!= 0) {

    this-&gt;_str = new char[strlen(arg._str) + 1];

    strcpy(this-&gt;_str, arg._str);

    _length = arg._length;

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

  if (this == &amp;arg)

   return *this;

  if (this-&gt;_length &gt;= arg._length) {// *this足夠大

    if (arg._str != 0)

      strcpy(this-&gt;_str, arg._str);

    else

      this-&gt;_str = 0;

    this-&gt;_length = arg._length;

    return *this;

  // *this沒有足夠的空間,_arg更大.

  delete [] _str; // 安全

  this-&gt;_length = arg.size();

  if (_length) {

    strcpy(_str, arg._str);

  else _str = 0;

  return *this;  // 總是這樣做

tstring&amp; tstring::operator=(const char* s)

  if (s == 0 || *s == 0) { // 源數組為空,讓“this”也為空。

    delete [] _str;

    _length = 0; _str = 0;

    _str = 0;

 int slength = strlen(s);

 if (this-&gt;_length &gt;= slength) { //*this足夠大

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

  this-&gt;_length = slength;

  return *this;

 // *this沒有足夠的空間,_arg更大。

 delete [] _str; // 安全

 this-&gt;_length = slength;

 _str = new char[_length + 1];

 strcpy(_str, s);

 return *this;

tstring&amp; tstring::operator=(char chartoassign)

 char s[2];

 s[0] = chartoassign;

 s[1] = ‘0’;

 // 使用其他指派操作符

 return (*this = s);

int tstring::size() const { return _length; }

tstring&amp; tstring::operator+=(const tstring&amp; arg)

  if (arg.size()) {  // 成員函數可調用其他成員函數

    _length = arg.size() + this-&gt;size();

    char *newstr = new char[_length + 1];

    if (this-&gt;size())  // 如果原始值不是null字元串

      strcpy(newstr, _str);

      *newstr = ‘0’;

    strcat(newstr, arg._str); // 附上參數字元串

    delete [] _str;  // 丢棄原始的記憶體

    _str = newstr;   // 這是建立的新字元串

tstring operator+(const tstring&amp; first, const tstring&amp; second)

  tstring result = first;

  result += second; // 調用operator+=成員函數

  return result;

bool operator==(const tstring&amp; first, const tstring&amp; second)

  const char* fp = first.c_str(); // 調用成員函數

  const char* sp = second.c_str();

  if (fp == 0 &amp;&amp; sp == 0) return 1;

  if (fp == 0 &amp;&amp; sp) return -1;

  if (fp &amp;&amp; sp == 0) return 1;

  return ( strcmp(fp, sp) == 0); // strcmp是一個庫函數

bool operator!=(const tstring&amp; first, const tstring&amp; second)

{ return !(first == second); }  // 複用operator==

// 其他比較操作符的實作類似operator== ,

// 為了簡潔代碼,未在此處顯示它們。

char tstring::operator()(unsigned n) const

  if (n &lt; this-&gt;size())

    return this-&gt;_str[n]; // 傳回下标為n的字元

  return 0;

const char&amp; tstring::operator[](unsigned n)const

  cout &lt;&lt; “invalid subscript: ” &lt;&lt; n &lt;&lt; endl;

 exit(-1);  // 應該在此處抛出異常

  return _str[0];  // 為編譯器減輕負擔(從不執行此行代碼)

// 将每個字元變成小寫

tstring&amp; tstring::tolower()

  // 使用tolower庫函數

  if (_str &amp;&amp; *_str) {

    char *p = _str;

    while (*p) {

      p = tolower(p);

      ++p;

    }

   }

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

tstring tstring::operator()(unsigned posn, unsigned len) const

  int sz = size(); // 源的大小

  if (posn &gt; sz) return “ ”; // 空字元串

  if (posn + len &gt; sz) len = sz – posn;

 tstring result;

  if (len) {

    result._str = new char[len+1];

    strncpy(result._str, _str + posn, len);

    result._length = len;

    result._str[len] = ‘0’;

ostream&amp; operator&lt;&lt;(ostream&amp; o, const tstring&amp; s)

  if (s.c_str())

    o &lt;&lt; s.c_str();

   return o;

istream&amp; operator&gt;&gt;(istream&amp; stream, tstring&amp; s)

  char c;

  s = “ ”;

  while (stream.get(c) &amp;&amp; isspace(c))

    ;// 什麼也不做

  if (stream) { // stream正常的話,

    // 讀取字元直至遇到空白

    do {

    s += c;

    } while (stream.get(c) &amp;&amp; !isspace(c));

    if (stream)  // 未讀取額外字元

      stream.putback(c);

  return stream;

1譯者注:伺服器指一個管理資源并為使用者提供服務的計算機軟體,另外,運作這樣軟體的計算機或計算機系統也被稱為伺服器。這裡,作者為區分兩種伺服器,用server machine表示運作伺服器的計算機,用license server表示許可證伺服器。

2觀衆需要為觀看(訂閱)固定數量的節目支付費用。這些節目包括最新的電影、體育節目甚至是直播,或者音樂會。

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

繼續閱讀