Virtual是C++ OO(面向?qū)ο髾C(jī)制)機(jī)制中很重要的一個(gè)關(guān)鍵字。虛函數(shù)就是因?yàn)槌蓡T函數(shù)加了關(guān)鍵字virtual,可見(jiàn)它的重要性。
只要是學(xué)過(guò)C++的人都知道在類(lèi)Base中加了Virtual關(guān)鍵字的函數(shù)就是虛擬函數(shù)(例如函數(shù)print),于是在Base的派生類(lèi)Derived中就可以通過(guò)重寫(xiě)虛擬函數(shù)來(lái)實(shí)現(xiàn)對(duì)基類(lèi)虛擬函數(shù)的覆蓋。當(dāng)基類(lèi)Base的指針point指向派生類(lèi)Derived的對(duì)象時(shí),對(duì)point的print函數(shù)的調(diào)用實(shí)際上是調(diào)用了Derived的print函數(shù)而不是Base的print函數(shù)。這是面向?qū)ο笾械亩鄳B(tài)性的體現(xiàn)。(關(guān)于虛擬機(jī)制是如何實(shí)現(xiàn)的,參見(jiàn)Inside the C++ Object Model ,Addison Wesley 1996)
- //---------------------------------------------------------
- class Base
- {
- public:Base(){}
- public:
- virtual void print(){cout<<"Base";}
- };
- class Derived:public Base
- {
- public:Derived(){}
- public:
- void print(){cout<<"Derived";}
- };
- int main()
- {
- Base *point=new Derived();
- point->print();
- }
- //---------------------------------------------------------
- Output:
- Derived
這也許會(huì)使人聯(lián)想到函數(shù)的重載,但稍加對(duì)比就會(huì)發(fā)現(xiàn)兩者是完全不同的:
(1) 重載的幾個(gè)函數(shù)必須在同一個(gè)類(lèi)中;
覆蓋的函數(shù)必須在有繼承關(guān)系的不同的類(lèi)中
(2) 覆蓋的幾個(gè)函數(shù)必須函數(shù)名、參數(shù)、返回值都相同;
重載的函數(shù)必須函數(shù)名相同,參數(shù)不同。參數(shù)不同的目的就是為了在函數(shù)調(diào)用的時(shí)候編譯器能夠通過(guò)參數(shù)來(lái)判斷程序是在調(diào)用的哪個(gè)函數(shù)。這也就很自然地解釋了為什么函數(shù)不能通過(guò)返回值不同來(lái)重載,因?yàn)槌绦蛟谡{(diào)用函數(shù)時(shí)很有可能不關(guān)心返回值,編譯器就無(wú)法從代碼中看出程序在調(diào)用的是哪個(gè)函數(shù)了。
(3) 覆蓋的函數(shù)前必須加關(guān)鍵字Virtual;
重載和Virtual沒(méi)有任何瓜葛,加不加都不影響重載的運(yùn)作。
關(guān)于C++的隱藏規(guī)則(引用自《高質(zhì)量C++/C 編程指南》林銳 2001):
(1)如果派生類(lèi)的函數(shù)與基類(lèi)的函數(shù)同名,但是參數(shù)不同。此時(shí),不論有無(wú)virtual
關(guān)鍵字,基類(lèi)的函數(shù)將被隱藏(注意別與重載混淆)。
(2)如果派生類(lèi)的函數(shù)與基類(lèi)的函數(shù)同名,并且參數(shù)也相同,但是基類(lèi)函數(shù)沒(méi)有virtual
關(guān)鍵字。此時(shí),基類(lèi)的函數(shù)被隱藏(注意別與覆蓋混淆)。
這里,林銳博士好像犯了個(gè)錯(cuò)誤。C++并沒(méi)有隱藏規(guī)則,林銳博士所總結(jié)的隱藏規(guī)則是他錯(cuò)誤地理解C++多態(tài)性所致。下面請(qǐng)看林銳博士給出的隱藏規(guī)則的例證:
- #include <iostream.h>
- class Base
- {
- public:
- virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
- void g(float x){ cout << "Base::g(float) " << x << endl; }
- void h(float x){ cout << "Base::h(float) " << x << endl; }
- };
- class Derived : public Base
- {
- public:
- virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
- void g(int x){ cout << "Derived::g(int) " << x << endl; }
- void h(float x){ cout << "Derived::h(float) " << x << endl; }
- };
- void main(void)
- {
- Derived d;
- Base *pb = &d;
- Derived *pd = &d;
- // Good : behavior depends solely on type of the object
- pb->f(3.14f); // Derived::f(float) 3.14
- pd->f(3.14f); // Derived::f(float) 3.14
- // Bad : behavior depends on type of the pointer
- pb->g(3.14f); // Base::g(float) 3.14
- pd->g(3.14f); // Derived::g(int) 3 (surprise!)
- // Bad : behavior depends on type of the pointer
- pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
- pd->h(3.14f); // Derived::h(float) 3.14
- }
林銳博士認(rèn)為bp 和dp 指向同一地址,按理說(shuō)運(yùn)行結(jié)果應(yīng)該是相同的,而事實(shí)上運(yùn)行結(jié)果不同,所以他把原因歸結(jié)為C++的隱藏規(guī)則,其實(shí)這一觀點(diǎn)是錯(cuò)的。決定bp和dp調(diào)用函數(shù)運(yùn)行結(jié)果的不是他們指向的地址,而是他們的指針類(lèi)型?!爸挥性谕ㄟ^(guò)基類(lèi)指針或引用間接指向派生類(lèi)子類(lèi)型時(shí)多態(tài)性才會(huì)起作用”(C++ Primer 3rd Edition)。pb是基類(lèi)指針,pd是派生類(lèi)指針,pd的所有函數(shù)調(diào)用都只是調(diào)用自己的函數(shù),和多態(tài)性無(wú)關(guān),所以pd的所有函數(shù)調(diào)用的結(jié)果都輸出Derived::是完全正常的;pb的函數(shù)調(diào)用如果有virtual則根據(jù)多態(tài)性調(diào)用派生類(lèi)的,如果沒(méi)有virtual則是正常的靜態(tài)函數(shù)調(diào)用,還是調(diào)用基類(lèi)的,所以有virtual的f函數(shù)調(diào)用輸出Derived::,其它兩個(gè)沒(méi)有virtual則還是輸出Base::很正常啊,nothing surprise!
所以并沒(méi)有所謂的隱藏規(guī)則,雖然《高質(zhì)量C++/C 編程指南》是本很不錯(cuò)的書(shū),可大家不要迷信哦。記住“只有在通過(guò)基類(lèi)指針或引用間接指向派生類(lèi)子類(lèi)型時(shí)多態(tài)性才會(huì)起作用”。
純虛函數(shù):
C++語(yǔ)言為我們提供了一種語(yǔ)法結(jié)構(gòu),通過(guò)它可以指明,一個(gè)虛擬函數(shù)只是提供了一個(gè)可被子類(lèi)型改寫(xiě)的接口。但是,它本身并不能通過(guò)虛擬機(jī)制被調(diào)用。這就是純虛擬函數(shù)(pure virtual function)。 純虛擬函數(shù)的聲明如下所示:
- class Query {
- public:
- // 聲明純虛擬函數(shù)
- virtual ostream& print( ostream&=cout ) const = 0;
- // ...
- };
這里函數(shù)聲明后面緊跟賦值0。
包含(或繼承)一個(gè)或多個(gè)純虛擬函數(shù)的類(lèi)被編譯器識(shí)別為抽象基類(lèi)。試圖創(chuàng)建一個(gè)抽象基類(lèi)的獨(dú)立類(lèi)對(duì)象會(huì)導(dǎo)致編譯時(shí)刻錯(cuò)誤。(類(lèi)似地通過(guò)虛擬機(jī)制調(diào)用純虛擬函數(shù)也是錯(cuò)誤的例如)
- // Query 聲明了純虛擬函數(shù)
- // 所以, 程序員不能創(chuàng)建獨(dú)立的 Query 類(lèi)對(duì)象
- // ok: NameQuery 中的 Query 子對(duì)象
- Query *pq = new NameQuery( "Nostromo" );
- // 錯(cuò)誤: new 表達(dá)式分配 Query 對(duì)象
- Query *pq2 = new Query;
抽象基類(lèi)只能作為子對(duì)象出現(xiàn)在后續(xù)的派生類(lèi)中。
如果只知道virtual加在函數(shù)前,那對(duì)virtual只了解了一半,virtual還有一個(gè)重要用法是virtual public,就是虛擬繼承。虛擬繼承在C++ Primer中有詳細(xì)的描述,下面稍作修改的闡釋一下:
在缺省情況下C++中的繼承是“按值組合”的一種特殊情況。當(dāng)我們寫(xiě)
- class Bear : public ZooAnimal { ... };
每個(gè)Bear 類(lèi)對(duì)象都含有其ZooAnimal 基類(lèi)子對(duì)象的所有非靜態(tài)數(shù)據(jù)成員以及在Bear中聲明的非靜態(tài)數(shù)據(jù)成員類(lèi)似地當(dāng)派生類(lèi)自己也作為一個(gè)基類(lèi)對(duì)象時(shí)如:
- class PolarBear : public Bear { ... };
則PolarBear 類(lèi)對(duì)象含有在PolarBear 中聲明的所有非靜態(tài)數(shù)據(jù)成員以及其Bear 子對(duì)象的所有非靜態(tài)數(shù)據(jù)成員和ZooAnimal 子對(duì)象的所有非靜態(tài)數(shù)據(jù)成員。在單繼承下這種由繼承支持的特殊形式的按值組合提供了最有效的最緊湊的對(duì)象表示。在多繼承下當(dāng)一個(gè)基類(lèi)在派生層次中出現(xiàn)多次時(shí)就會(huì)有問(wèn)題最主要的實(shí)際例子是iostream 類(lèi)層次結(jié)構(gòu)。ostream 和istream 類(lèi)都從抽象ios 基類(lèi)派生而來(lái),而iostream 類(lèi)又是從ostream 和istream 派生
- class iostream :public istream, public ostream { ... };
缺省情況下,每個(gè)iostream 類(lèi)對(duì)象含有兩個(gè)ios 子對(duì)象:在istream 子對(duì)象中的實(shí)例以及在ostream 子對(duì)象中的實(shí)例。這為什么不好?從效率上而言,存儲(chǔ)ios 子對(duì)象的兩個(gè)復(fù)本,浪費(fèi)了存儲(chǔ)區(qū),因?yàn)閕ostream 只需要一個(gè)實(shí)例。而且,ios 構(gòu)造函數(shù)被調(diào)用了兩次每個(gè)子對(duì)象一次。更嚴(yán)重的問(wèn)題是由于兩個(gè)實(shí)例引起的二義性。例如,任何未限定修飾地訪問(wèn)ios 的成員都將導(dǎo)致編譯時(shí)刻錯(cuò)誤:到底訪問(wèn)哪個(gè)實(shí)例?如果ostream 和istream 對(duì)其ios 子對(duì)象的初始化稍稍不同,會(huì)怎樣呢?怎樣通過(guò)iostream 類(lèi)保證這一對(duì)ios 值的一致性?在缺省的按值組合機(jī)制下,真的沒(méi)有好辦法可以保證這一點(diǎn)。
C++語(yǔ)言的解決方案是,提供另一種可替代按“引用組合”的繼承機(jī)制虛擬繼承(virtual inheritance )在虛擬繼承下只有一個(gè)共享的基類(lèi)子對(duì)象被繼承而無(wú)論該基類(lèi)在派生層次
中出現(xiàn)多少次共享的基類(lèi)子對(duì)象被稱(chēng)為虛擬基類(lèi)。
通過(guò)用關(guān)鍵字virtual 修政一個(gè)基類(lèi)的聲明可以將它指定為被虛擬派生。例如,下列聲明使得ZooAnimal 成為Bear 和Raccoon 的虛擬基類(lèi):
- // 關(guān)鍵字 public 和 virtual的順序不重要
- class Bear : public virtual ZooAnimal { ... };
- class Raccoon : virtual public ZooAnimal { ... };
虛擬派生不是基類(lèi)本身的一個(gè)顯式特性,而是它與派生類(lèi)的關(guān)系如前面所說(shuō)明的,虛擬繼承提供了“按引用組合”。也就是說(shuō),對(duì)于子對(duì)象及其非靜態(tài)成員的訪問(wèn)是間接進(jìn)行的。這使得在多繼承情況下,把多個(gè)虛擬基類(lèi)子對(duì)象組合成派生類(lèi)中的一個(gè)共享實(shí)例,從而提供了必要的靈活性。同時(shí),即使一個(gè)基類(lèi)是虛擬的,我們?nèi)匀豢梢酝ㄟ^(guò)該基類(lèi)類(lèi)型的指針或引用,來(lái)操縱派生類(lèi)的對(duì)象。
沒(méi)有虛函數(shù)的C++不能面向?qū)ο?。從商業(yè)的角度看,面向?qū)ο竽苁瓜到y(tǒng)具有可擴(kuò)展性和可適應(yīng)性,但只有C++類(lèi)的語(yǔ)法而沒(méi)有面向?qū)ο蟮脑?,就不?huì)減少維護(hù)成本,而實(shí)際上會(huì)增加成本。所以沒(méi)有虛函數(shù)是萬(wàn)萬(wàn)不能的,而關(guān)鍵字virtual則是關(guān)鍵。




