More Effective C++ 條款25 (轉)

worldblog發表於2007-12-11
More Effective C++ 條款25 (轉)[@more@]

技巧:namespace prefix = o ns = "urn:schemas--com::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 components;

};

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

ectratio="t">

在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 components;

};

 

NewsLetter::NewsLetter(const NewsLetter& rhs)

{

  // 遍歷整個rhs連結串列,使用每個元素的虛擬複製建構函式

  // 把元素複製進這個物件的component連結串列。

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

  for (list::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<TextBlock 或 Graphic成員函式。

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

class NLComponent {

public:

  // 對輸出運算子的不尋常的宣告

  virtual ostream& operator<

  ...

};

 

class TextBlock: public NLComponent {

public:

  // 虛擬輸出運算子(同樣不尋常)

  virtual ostream& operator<

};

 

class Graphic: public NLComponent {

public:

  // 虛擬輸出運算子 (讓就不尋常)

  virtual ostream& operator<

};

 

TextBlock t;

Graphic g;

 

...

 

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

   //把t列印到cout中。

  // 不尋常的語法

 

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

  //把g列印到cout中。

//不尋常的語法

類的使用者得把stream物件放到<TextBlock 和 Graphic類,但是如果我們這樣做,就不能再把它宣告為虛擬了。)

另一種方法是為列印操作宣告一個虛擬函式(例如print)把它定義在TextBlockGraphic類裡。但是如果這樣,列印TextBlockGraphic物件的語法就與使用operator<

這些解決方法都不很令人滿意。我們想要的是一個稱為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<

{

  return c.print(s);

}

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

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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-991404/,如需轉載,請註明出處,否則將追究法律責任。

相關文章