More Effective C++ 條款25

技巧<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

本書涉及的大多數內容都是編程的指導準則。這些準則雖是重要的,但是程序員不能單靠準則生活。有一個很早以前的卡通片叫做“菲利貓”(Felix the Cat), 菲利貓無論何時遇到困難,它都會拿它的trick包。如果一個卡通角色都有一個trick包,那麼C++程序員就更應該有了。把這一章想成你的trick包的啓動器。

當設計C++軟件時,總會再三地受到一些問題的困擾。你如何讓構造函數和非成員函數具有虛擬函數的特點?你如何限制一個類的實例的數量?你如何防止在堆中建立對象呢?你如何又能確保把對象建立在堆中呢?其它一些類的成員函數無論何時被調用,你如何能建立一個對象並讓它自動地完成一些操作?你如何能讓不同的對象共享數據結構,而讓每個使用者以爲它們每一個都擁有自己的拷貝?你如何區分operator[]的讀操作和寫操作?你如何建立一個虛函數,其行爲特性依賴於不同對象的動態類型?

所有這些問題(還有更多)都在本章得到解答,在本章裏我敘述的都是C++程序員普遍遇到的問題,且解決方法也是已被證實了的。我把這些解決方法叫做技巧,不過當它們以程式化的風格(stylized fashion)被歸檔時,也被做爲idiom和pattern。不管你把它稱做什麼,在你日復一日地從事軟件開發工作時,下面這些信息都將使你受益。它會使你相信無論你做什麼,總可以用C++來完成它。

 

條款25:將構造函數和非成員函數虛擬化

從字面來看,談論“虛擬構造函數”沒有意義。當你有一個指針或引用,但是不知道其指向對象的真實類型是什麼時,你可以調用虛擬函數來完成特定類型(type-specific)對象的行爲。僅當你還沒擁有一個對象但是你確切地知道想要對象的類型時,你纔會調用構造函數。那麼虛擬構造函數又從何談起呢?

很簡單。儘管虛擬構造函數看起來好像沒有意義,其實它們有非常大的用處(如果你認爲沒有意義的想法就沒有用處,那麼你怎麼解釋現代物理學的成就呢?)(因爲現代物理學的主要成就是狹義、廣義相對論,量子力學,這些理論看起來都好象很荒謬,不好理解。  譯者注)。例如假設你編寫一個程序,用來進行新聞報道的工作,一條新聞報道由文字或圖片組成。你可以這樣管理它們:

class NLComponent {               //用於 newsletter components

public:                           // 的抽象基類

 

  ...                             //包含只少一個純虛函數

};  

 

class TextBlock: public NLComponent {

public:

  ...                             // 不包含純虛函數

}; 

 

class Graphic: public NLComponent {

public:

  ...                             // 不包含純虛函數

};

 

class NewsLetter {                // 一個 newsletter 對象

public:                           // NLComponent 對象

  ...                             // 的鏈表組成

 

private:

  list<NLComponent*> components;

};

類之間的關係如下圖所示:

<?xml:namespace prefix = v ns = "urn:schemas-microsoft-com:vml" />

NewsLetter中使用的list類是一個標準模板類(STL),STL是標準C++類庫的一部分(參見Effective C++條款49和條款35)。list類型對象的行爲特性有些象雙向鏈表,儘管它沒有以這種方法來實現。

對象NewLetter不運行時就會存儲在磁盤上。爲了能夠通過位於磁盤的替代物來建立Newsletter對象,讓NewLetter的構造函數帶有istream參數是一種很方便的方法。當構造函數需要一些核心的數據結構時,它就從流中讀取信息:

class NewsLetter {

public:

  NewsLetter(istream& str);

  ...

};

此構造函數的僞代碼是這樣的:

NewsLetter::NewsLetter(istream& str)

{

  while (str) {

    str讀取下一個component對象;

 

    把對象加入到newsletter components

   對象的鏈表中去;

  }

}

或者把這種技巧用於另一個獨立出來的函數叫做readComponent如下所示

class NewsLetter {

public:

  ...

 

private:

  // 爲建立下一個NLComponent對象str讀取數據,

  // 建立component 並返回一個指針。

  static NLComponent * readComponent(istream& str);

  ...

};

 

NewsLetter::NewsLetter(istream& str)

{

  while (str) {

    // readComponent返回的指針添加到components鏈表的最後,

    // "push_back" 一個鏈表的成員函數,用來在鏈表最後進行插入操作。

    components.push_back(readComponent(str));

  }

}

考慮一下readComponent所做的工作。它根據所讀取的數據建立了一個新對象或是TextBlock或是Graphic。因爲它能建立新對象,它的行爲與構造函數相似,而且因爲它能建立不同類型的對象,我們稱它爲虛擬構造函數。虛擬構造函數是指能夠根據輸入給它的數據的不同而建立不同類型的對象。虛擬構造函數在很多場合下都有用處,從磁盤(或者通過網絡連接,或者從磁帶機上)讀取對象信息只是其中的一個應用。

還有一種特殊種類的虛擬構造函數――虛擬拷貝構造函數――也有着廣泛的用途。虛擬拷貝構造函數能返回一個指針,指向調用該函數的對象的新拷貝。因爲這種行爲特性,虛擬拷貝構造函數的名字一般都是copySelf,cloneSelf或者是象下面這樣就叫做clone。很少會有函數能以這麼直接的方式實現它:

class NLComponent {

public:

  // declaration of virtual copy constructor

  virtual NLComponent * clone() const = 0;

  ...

 

};

 

class TextBlock: public NLComponent {

public:

  virtual TextBlock * clone() const         // virtual copy

  { return new TextBlock(*this); }          // constructor

  ...

 

};

 

class Graphic: public NLComponent {

public:

  virtual Graphic * clone() const            // virtual copy

  { return new Graphic(*this); }             // constructor

  ...

 

};

正如我們看到的,類的虛擬拷貝構造函數只是調用它們真正的拷貝構造函數。因此”拷貝”的含義與真正的拷貝構造函數相同。如果真正的拷貝構造函數只做了簡單的拷貝,那麼虛擬拷貝構造函數也做簡單的拷貝。如果真正的拷貝構造函數做了全面的拷貝,那麼虛擬拷貝構造函數也做全面的拷貝。如果真正的拷貝構造函數做一些奇特的事情,象引用計數或copy-on-write(參見條款29),那麼虛擬構造函數也這麼做。完全一致,太棒了。

注意上述代碼的實現利用了最近才被採納的較寬鬆的虛擬函數返回值類型規則。被派生類重定義的虛擬函數不用必須與基類的虛擬函數具有一樣的返回類型。如果函數的返回類型是一個指向基類的指針(或一個引用),那麼派生類的函數可以返回一個指向基類的派生類的指針(或引用)。這不是C++的類型檢查上的漏洞,它使得又可能聲明象虛擬構造函數這樣的函數。這就是爲什麼TextBlock的clone函數能夠返回TextBlock*和Graphic的clone能夠返回Graphic*的原因,即使NLComponentclone返回值類型爲NLComponent*

NLComponent中的虛擬拷貝構造函數能讓實現NewLetter(正常的)拷貝構造函數變得很容易:

class NewsLetter {

public:

  NewsLetter(const NewsLetter& rhs);

  ...

 

private:

  list<NLComponent*> components;

};

 

NewsLetter::NewsLetter(const NewsLetter& rhs)

{

  // 遍歷整個rhs鏈表,使用每個元素的虛擬拷貝構造函數

  // 把元素拷貝進這個對象的component鏈表。

  // 有關下面代碼如何運行的詳細情況,請參見條款35.

  for (list<NLComponent*>::const_iterator it =

          rhs.components.begin();

       it != rhs.components.end();

       ++it) {

 

    // "it" 指向rhs.components的當前元素,調用元素的clone函數,

// 得到該元素的一個拷貝,並把該拷貝放到

//這個對象的component鏈表的尾端。

    components.push_back((*it)->clone());

  }

}

如果你對標準模板庫(STL)不熟悉,這段代碼可能有些令人費解,不過原理很簡單:遍歷被拷貝的NewsLetter對象中的整個component鏈表,調用鏈表內每個元素對象的虛擬構造函數。我們在這裏需要一個虛擬構造函數,因爲鏈表中包含指向NLComponent對象的指針,但是我們知道其實每一個指針不是指向TextBlock對象就是指向Graphic對象。無論它指向誰,我們都想進行正確的拷貝操作,虛擬構造函數能夠爲我們做到這點。

虛擬化非成員函數

就象構造函數不能真的成爲虛擬函數一樣,非成員函數也不能成爲真正的虛擬函數(參加Effective C++ 條款19)。然而,既然一個函數能夠構造出不同類型的新對象是可以理解的,那麼同樣也存在這樣的非成員函數,可以根據參數的不同動態類型而其行爲特性也不同。例如,假設你想爲TextBlockGraphic對象實現一個輸出操作符。顯而易見的方法是虛擬化這個輸出操作符。但是輸出操作符是operator<<,函數把ostream&做爲它的左參數(left-hand argument)(即把它放在函數參數列表的左邊  譯者注),這就不可能使該函數成爲TextBlock Graphic成員函數。

(這樣做也可以,不過看一看會發生什麼:

class NLComponent {

public:

  // 對輸出操作符的不尋常的聲明

  virtual ostream& operator<<(ostream& str) const = 0;

  ...

};

 

class TextBlock: public NLComponent {

public:

  // 虛擬輸出操作符(同樣不尋常)

  virtual ostream& operator<<(ostream& str) const;

};

 

class Graphic: public NLComponent {

public:

  // 虛擬輸出操作符 (讓就不尋常)

  virtual ostream& operator<<(ostream& str) const;

};

 

TextBlock t;

Graphic g;

 

...

 

t << cout;                                  // 通過virtual operator<<

                                            //t打印到cout中。

                                            // 不尋常的語法

 

g << cout;                                  //通過virtual operator<<

                                            //g打印到cout中。

//不尋常的語法

類的使用者得把stream對象放到<<符號的右邊,這與輸出操作符一般的用發相反。爲了能夠回到正常的語法上來,我們必須把operator<<移出TextBlock Graphic類,但是如果我們這樣做,就不能再把它聲明爲虛擬了。)

另一種方法是爲打印操作聲明一個虛擬函數(例如print)把它定義在TextBlock Graphic類裏。但是如果這樣,打印TextBlock Graphic對象的語法就與使用operator<<做爲輸出操作符的其它類型的對象不一致了,

這些解決方法都不很令人滿意。我們想要的是一個稱爲operator<<的非成員函數,其具有象print虛擬函數的行爲特性。有關我們想要什麼的描述實際上已經很接近如何得到它的描述。我們定義operator<< print函數,讓前者調用後者!

class NLComponent {

public:

  virtual ostream& print(ostream& s) const = 0;

  ...

 

};

 

class TextBlock: public NLComponent {

public:

  virtual ostream& print(ostream& s) const;

  ...

 

};

 

class Graphic: public NLComponent {

public:

  virtual ostream& print(ostream& s) const;

  ...

 

};

 

inline

ostream& operator<<(ostream& s, const NLComponent& c)

{

  return c.print(s);

}

具有虛擬行爲的非成員函數很簡單。你編寫一個虛擬函數來完成工作,然後再寫一個非虛擬函數,它什麼也不做只是調用這個虛擬函數。爲了避免這個句法花招引起函數調用開銷,你當然可以內聯這個非虛擬函數(參見Effective C++ 條款33)。

現在你知道如何根據它們的一個參數讓非成員函數虛擬化,你可能想知道是否可能讓它們根據一個以上的參數虛擬化呢?可以,但是不是很容易。有多困難呢?參見條款31;它將專門論述這個問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章