從字面來看,談?wù)摗疤摂M構(gòu)造函數(shù)”沒有意義。當(dāng)有一個指針或引用,但是不知道其指向?qū)ο蟮恼鎸?shí)類型是什么時,可以調(diào)用虛擬函數(shù)來完成特定類型(type-specific)對象的行為。僅當(dāng)還沒擁有一個對象但是又確切地知道想要的對象的類型時,才會調(diào)用構(gòu)造函數(shù)。那么虛擬構(gòu)造函數(shù)又從何談起呢?
很簡單。盡管虛擬構(gòu)造函數(shù)看起來好像沒有意義,其實(shí)它們有非常大的用處.例如,假設(shè)編寫一個程序,用來進(jìn)行新聞報道的工作,每一條新聞報道都由文字或圖片組成??梢赃@樣管理它們:
class NLComponent {//用于 newsletter components
public:// 的抽象基類
... //包含至少一個純虛函數(shù)
};
class TextBlock: public NLComponent {
public:
... // 不包含純虛函數(shù)
};
class Graphic: public NLComponent {
public:
... // 不包含純虛函數(shù)
};
class NewsLetter { // 一個 newsletter 對象
public:// 由NLComponent 對象
... // 的鏈表組成
private:
list components;
};
在NewsLetter中使用的list類是一個標(biāo)準(zhǔn)模板類(STL)。list類型對象的行為特性有些象雙向鏈表,盡管它沒有以這種方法來實(shí)現(xiàn)。對象NewLetter不運(yùn)行時就會存儲在磁盤上。為了能夠通過位于磁盤的替代物來建立Newsletter對象,讓NewLetter的構(gòu)造函數(shù)帶有istream參數(shù)是一種很方便的方法。當(dāng)構(gòu)造函數(shù)需要一些核心的數(shù)據(jù)結(jié)構(gòu)時,它就從流中讀取信息:
class NewsLetter {
public:
NewsLetter(istream& str);
...
};
此構(gòu)造函數(shù)的偽代碼是這樣的:
NewsLetter::NewsLetter(istream& str)
{
while (str) {
從str讀取下一個component對象;
把對象加入到newsletter的 components對象的鏈表中去;
}
}
或者,把這種技巧用于另一個獨(dú)立出來的函數(shù)叫做readComponent,如下所示:
class NewsLetter {
public:
...
private:
// 為建立下一個NLComponent對象從str讀取數(shù)據(jù),
// 建立component 并返回一個指針。
static NLComponent * readComponent(istream& str);
...
};
NewsLetter::NewsLetter(istream& str)
{
while (str) {
// 把readComponent返回的指針添加到components鏈表的最后,
// \"push_back\" 一個鏈表的成員函數(shù),用來在鏈表最后進(jìn)行插入操作。
components.push_back(readComponent(str));
}
}
考慮一下readComponent所做的工作。它根據(jù)所讀取的數(shù)據(jù)建立了一個新對象,或是TextBlock或是Graphic。因?yàn)樗芙⑿聦ο螅男袨榕c構(gòu)造函數(shù)相似,而且因?yàn)樗芙⒉煌愋偷膶ο?,我們稱它為虛擬構(gòu)造函數(shù)。虛擬構(gòu)造函數(shù)是指能夠根據(jù)輸入給它的數(shù)據(jù)的不同而建立不同類型的對象。虛擬構(gòu)造函數(shù)在很多場合下都有用處,從磁盤(或者通過網(wǎng)絡(luò)連接,或者從磁帶機(jī)上)讀取對象信息只是其中的一個應(yīng)用。
還有一種特殊種類的虛擬構(gòu)造函數(shù)――虛擬拷貝構(gòu)造函數(shù)――也有著廣泛的用途。虛擬拷貝構(gòu)造函數(shù)能返回一個指針,指向調(diào)用該函數(shù)的對象的新拷貝。因?yàn)檫@種行為特性,虛擬拷貝構(gòu)造函數(shù)的名字一般都是copySelf,cloneSelf或者是象下面這樣就叫做clone。很少會有函數(shù)能以這么直接的方式實(shí)現(xiàn)它:
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
...
}; 正如我們看到的,類的虛擬拷貝構(gòu)造函數(shù)只是調(diào)用它們真正的拷貝構(gòu)造函數(shù)。因此“拷貝”的含義與真正的拷貝構(gòu)造函數(shù)相同。如果真正的拷貝構(gòu)造函數(shù)只做了簡單的拷貝,那么虛擬拷貝構(gòu)造函數(shù)也做簡單的拷貝。如果真正的拷貝構(gòu)造函數(shù)做了全面的拷貝,那么虛擬拷貝構(gòu)造函數(shù)也做全面的拷貝。如果真正的拷貝構(gòu)造函數(shù)做一些奇特的事情,象引用計(jì)數(shù)或copy-on-write,那么虛擬構(gòu)造函數(shù)也這么做。
注意上述代碼的實(shí)現(xiàn)利用了最近才被采納的較寬松的虛擬函數(shù)返回值類型規(guī)則。被派生類重定義的虛擬函數(shù)不用必須與基類的虛擬函數(shù)具有一樣的返回類型。如果函數(shù)的返回類型是一個指向基類的指針(或一個引用),那么派生類的函數(shù)可以返回一個指向基類的派生類的指針(或引用)。這不是C++的類型檢查上的漏洞,它使得有可能聲明象虛擬構(gòu)造函數(shù)這樣的函數(shù)。這就是為什么TextBlock的clone函數(shù)能夠返回TextBlock*和Graphic的clone能夠返回Graphic*的原因,即使NLComponent的clone返回值類型為NLComponent*。
在NLComponent中的虛擬拷貝構(gòu)造函數(shù)能讓實(shí)現(xiàn)NewLetter的(正常的)拷貝構(gòu)造函數(shù)變得很容易:
class NewsLetter {
public:
NewsLetter(const NewsLetter& rhs);
...
private:
list components;
};
NewsLetter::NewsLetter(const NewsLetter& rhs)
{
// 遍歷整個rhs鏈表,使用每個元素的虛擬拷貝構(gòu)造函數(shù)
// 把元素拷貝進(jìn)這個對象的component鏈表。
for (list::const_iterator it =
rhs.components.begin();
it != rhs.components.end();
++it) {
// \"it\" 指向rhs.components的當(dāng)前元素,調(diào)用元素的clone函數(shù),
// 得到該元素的一個拷貝,并把該拷貝放到
// 這個對象的component鏈表的尾端。
components.push_back((*it)->clone());
}
}
遍歷被拷貝的NewsLetter對象中的整個component鏈表,調(diào)用鏈表內(nèi)每個元素對象的虛擬構(gòu)造函數(shù)。我們在這里需要一個虛擬構(gòu)造函數(shù),因?yàn)殒湵碇邪赶騈LComponent對象的指針,但是我們知道其實(shí)每一個指針不是指向TextBlock對象就是指向Graphic對象。無論它指向誰,我們都想進(jìn)行正確的拷貝操作,虛擬構(gòu)造函數(shù)能夠?yàn)槲覀冏龅竭@點(diǎn)。
虛擬化非成員函數(shù)
就象構(gòu)造函數(shù)不能真的成為虛擬函數(shù)一樣,非成員函數(shù)也不能成為真正的虛擬函數(shù)。然而,既然一個函數(shù)能夠構(gòu)造出不同類型的新對象是可以理解的,那么同樣也存在這樣的非成員函數(shù),可以根據(jù)參數(shù)的不同動態(tài)類型而其行為特性也不同。例如,假設(shè)你想為TextBlock和Graphic對象實(shí)現(xiàn)一個輸出操作符。顯而易見的方法是虛擬化這個輸出操作符。但是輸出操作符是operator<<,函數(shù)把ostream&做為它的左參數(shù)(left-hand argument)(即把它放在函數(shù)參數(shù)列表的左邊),這就不可能使該函數(shù)成為TextBlock 或 Graphic成員函數(shù)。
(這樣做也可以,不過看一看會發(fā)生什么:
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<<
印到cout中。
//不尋常的語法
類的使用者得把stream對象放到<<符號的右邊,這與輸出操作符一般的用法相反。為了能夠回到正常的語法上來,我們必須把operator<<移出TextBlock 和 Graphic類,但是如果我們這樣做,就不能再把它聲明為虛擬了。)
另一種方法是為打印操作聲明一個虛擬函數(shù)(例如print)把它定義在TextBlock 和 Graphic類里。但是如果這樣,打印TextBlock 和 Graphic對象的語法就與使用operator<<做為輸出操作符的其它類型的對象不一致了,這些解決方法都不很令人滿意。我們想要的是一個稱為operator<<的非成員函數(shù),其具有象print虛擬函數(shù)的行為特性。有關(guān)我們想要什么的描述實(shí)際上已經(jīng)很接近如何得到它的描述。我們定義operator<< 和print函數(shù),讓前者調(diào)用后者!
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);
}
具有虛擬行為的非成員函數(shù)很簡單。編寫一個虛擬函數(shù)來完成工作,然后再寫一個非虛擬函數(shù),它什么也不做只是調(diào)用這個虛擬函數(shù)。為了避免這個句法花招引起函數(shù)調(diào)用開銷,當(dāng)然可以內(nèi)聯(lián)這個非虛擬函數(shù)。
現(xiàn)在你知道如何根據(jù)它們的一個參數(shù)讓非成員函數(shù)虛擬化,你可能想知道是否可能讓它們根據(jù)一個以上的參數(shù)虛擬化呢?可以,但是不是很容易。
很簡單。盡管虛擬構(gòu)造函數(shù)看起來好像沒有意義,其實(shí)它們有非常大的用處.例如,假設(shè)編寫一個程序,用來進(jìn)行新聞報道的工作,每一條新聞報道都由文字或圖片組成??梢赃@樣管理它們:
class NLComponent {//用于 newsletter components
public:// 的抽象基類
... //包含至少一個純虛函數(shù)
};
class TextBlock: public NLComponent {
public:
... // 不包含純虛函數(shù)
};
class Graphic: public NLComponent {
public:
... // 不包含純虛函數(shù)
};
class NewsLetter { // 一個 newsletter 對象
public:// 由NLComponent 對象
... // 的鏈表組成
private:
list
};
在NewsLetter中使用的list類是一個標(biāo)準(zhǔn)模板類(STL)。list類型對象的行為特性有些象雙向鏈表,盡管它沒有以這種方法來實(shí)現(xiàn)。對象NewLetter不運(yùn)行時就會存儲在磁盤上。為了能夠通過位于磁盤的替代物來建立Newsletter對象,讓NewLetter的構(gòu)造函數(shù)帶有istream參數(shù)是一種很方便的方法。當(dāng)構(gòu)造函數(shù)需要一些核心的數(shù)據(jù)結(jié)構(gòu)時,它就從流中讀取信息:
class NewsLetter {
public:
NewsLetter(istream& str);
...
};
此構(gòu)造函數(shù)的偽代碼是這樣的:
NewsLetter::NewsLetter(istream& str)
{
while (str) {
從str讀取下一個component對象;
把對象加入到newsletter的 components對象的鏈表中去;
}
}
或者,把這種技巧用于另一個獨(dú)立出來的函數(shù)叫做readComponent,如下所示:
class NewsLetter {
public:
...
private:
// 為建立下一個NLComponent對象從str讀取數(shù)據(jù),
// 建立component 并返回一個指針。
static NLComponent * readComponent(istream& str);
...
};
NewsLetter::NewsLetter(istream& str)
{
while (str) {
// 把readComponent返回的指針添加到components鏈表的最后,
// \"push_back\" 一個鏈表的成員函數(shù),用來在鏈表最后進(jìn)行插入操作。
components.push_back(readComponent(str));
}
}
考慮一下readComponent所做的工作。它根據(jù)所讀取的數(shù)據(jù)建立了一個新對象,或是TextBlock或是Graphic。因?yàn)樗芙⑿聦ο螅男袨榕c構(gòu)造函數(shù)相似,而且因?yàn)樗芙⒉煌愋偷膶ο?,我們稱它為虛擬構(gòu)造函數(shù)。虛擬構(gòu)造函數(shù)是指能夠根據(jù)輸入給它的數(shù)據(jù)的不同而建立不同類型的對象。虛擬構(gòu)造函數(shù)在很多場合下都有用處,從磁盤(或者通過網(wǎng)絡(luò)連接,或者從磁帶機(jī)上)讀取對象信息只是其中的一個應(yīng)用。
還有一種特殊種類的虛擬構(gòu)造函數(shù)――虛擬拷貝構(gòu)造函數(shù)――也有著廣泛的用途。虛擬拷貝構(gòu)造函數(shù)能返回一個指針,指向調(diào)用該函數(shù)的對象的新拷貝。因?yàn)檫@種行為特性,虛擬拷貝構(gòu)造函數(shù)的名字一般都是copySelf,cloneSelf或者是象下面這樣就叫做clone。很少會有函數(shù)能以這么直接的方式實(shí)現(xiàn)它:
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
...
}; 正如我們看到的,類的虛擬拷貝構(gòu)造函數(shù)只是調(diào)用它們真正的拷貝構(gòu)造函數(shù)。因此“拷貝”的含義與真正的拷貝構(gòu)造函數(shù)相同。如果真正的拷貝構(gòu)造函數(shù)只做了簡單的拷貝,那么虛擬拷貝構(gòu)造函數(shù)也做簡單的拷貝。如果真正的拷貝構(gòu)造函數(shù)做了全面的拷貝,那么虛擬拷貝構(gòu)造函數(shù)也做全面的拷貝。如果真正的拷貝構(gòu)造函數(shù)做一些奇特的事情,象引用計(jì)數(shù)或copy-on-write,那么虛擬構(gòu)造函數(shù)也這么做。
注意上述代碼的實(shí)現(xiàn)利用了最近才被采納的較寬松的虛擬函數(shù)返回值類型規(guī)則。被派生類重定義的虛擬函數(shù)不用必須與基類的虛擬函數(shù)具有一樣的返回類型。如果函數(shù)的返回類型是一個指向基類的指針(或一個引用),那么派生類的函數(shù)可以返回一個指向基類的派生類的指針(或引用)。這不是C++的類型檢查上的漏洞,它使得有可能聲明象虛擬構(gòu)造函數(shù)這樣的函數(shù)。這就是為什么TextBlock的clone函數(shù)能夠返回TextBlock*和Graphic的clone能夠返回Graphic*的原因,即使NLComponent的clone返回值類型為NLComponent*。
在NLComponent中的虛擬拷貝構(gòu)造函數(shù)能讓實(shí)現(xiàn)NewLetter的(正常的)拷貝構(gòu)造函數(shù)變得很容易:
class NewsLetter {
public:
NewsLetter(const NewsLetter& rhs);
...
private:
list
};
NewsLetter::NewsLetter(const NewsLetter& rhs)
{
// 遍歷整個rhs鏈表,使用每個元素的虛擬拷貝構(gòu)造函數(shù)
// 把元素拷貝進(jìn)這個對象的component鏈表。
for (list
rhs.components.begin();
it != rhs.components.end();
++it) {
// \"it\" 指向rhs.components的當(dāng)前元素,調(diào)用元素的clone函數(shù),
// 得到該元素的一個拷貝,并把該拷貝放到
// 這個對象的component鏈表的尾端。
components.push_back((*it)->clone());
}
}
遍歷被拷貝的NewsLetter對象中的整個component鏈表,調(diào)用鏈表內(nèi)每個元素對象的虛擬構(gòu)造函數(shù)。我們在這里需要一個虛擬構(gòu)造函數(shù),因?yàn)殒湵碇邪赶騈LComponent對象的指針,但是我們知道其實(shí)每一個指針不是指向TextBlock對象就是指向Graphic對象。無論它指向誰,我們都想進(jìn)行正確的拷貝操作,虛擬構(gòu)造函數(shù)能夠?yàn)槲覀冏龅竭@點(diǎn)。
虛擬化非成員函數(shù)
就象構(gòu)造函數(shù)不能真的成為虛擬函數(shù)一樣,非成員函數(shù)也不能成為真正的虛擬函數(shù)。然而,既然一個函數(shù)能夠構(gòu)造出不同類型的新對象是可以理解的,那么同樣也存在這樣的非成員函數(shù),可以根據(jù)參數(shù)的不同動態(tài)類型而其行為特性也不同。例如,假設(shè)你想為TextBlock和Graphic對象實(shí)現(xiàn)一個輸出操作符。顯而易見的方法是虛擬化這個輸出操作符。但是輸出操作符是operator<<,函數(shù)把ostream&做為它的左參數(shù)(left-hand argument)(即把它放在函數(shù)參數(shù)列表的左邊),這就不可能使該函數(shù)成為TextBlock 或 Graphic成員函數(shù)。
(這樣做也可以,不過看一看會發(fā)生什么:
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<<
印到cout中。
//不尋常的語法
類的使用者得把stream對象放到<<符號的右邊,這與輸出操作符一般的用法相反。為了能夠回到正常的語法上來,我們必須把operator<<移出TextBlock 和 Graphic類,但是如果我們這樣做,就不能再把它聲明為虛擬了。)
另一種方法是為打印操作聲明一個虛擬函數(shù)(例如print)把它定義在TextBlock 和 Graphic類里。但是如果這樣,打印TextBlock 和 Graphic對象的語法就與使用operator<<做為輸出操作符的其它類型的對象不一致了,這些解決方法都不很令人滿意。我們想要的是一個稱為operator<<的非成員函數(shù),其具有象print虛擬函數(shù)的行為特性。有關(guān)我們想要什么的描述實(shí)際上已經(jīng)很接近如何得到它的描述。我們定義operator<< 和print函數(shù),讓前者調(diào)用后者!
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);
}
具有虛擬行為的非成員函數(shù)很簡單。編寫一個虛擬函數(shù)來完成工作,然后再寫一個非虛擬函數(shù),它什么也不做只是調(diào)用這個虛擬函數(shù)。為了避免這個句法花招引起函數(shù)調(diào)用開銷,當(dāng)然可以內(nèi)聯(lián)這個非虛擬函數(shù)。
現(xiàn)在你知道如何根據(jù)它們的一個參數(shù)讓非成員函數(shù)虛擬化,你可能想知道是否可能讓它們根據(jù)一個以上的參數(shù)虛擬化呢?可以,但是不是很容易。

