天天看點

C++ Virtual詳解

virtual是c++ oo機制中很重要的一個關鍵字。隻要是學過c++的人都知道在類base中加了virtual關鍵字的函數就是虛拟函數(例如下面例子中的函數print),于是在base的派生類derived中就可以通過重寫虛拟函數來實作對基類虛拟函數的覆寫。當基類base的指針point指向派生類derived的對象時,對point的print函數的調用實際上是調用了derived的print函數而不是base的print函數。這是面向對象中的多态性的展現。(關于虛拟機制是如何實作的,參見inside the

c++ object model ,addison wesley 1996)

class base  

{  

public:base(){}  

public:  

       virtual void print(){cout<<"base";}  

};  

class derived:public base  

public:derived(){}  

       void print(){cout<<"derived";}  

int main()  

       base *point=new derived();  

       point->print();  

}   

//---------------------------------------------------------

output:

derived

這也許會使人聯想到函數的重載,但稍加對比就會發現兩者是完全不同的:

(1)      重載的幾個函數必須在同一個類中;

覆寫的函數必須在有繼承關系的不同的類中

(2)      覆寫的幾個函數必須函數名、參數、傳回值都相同;

重載的函數必須函數名相同,參數不同。參數不同的目的就是為了在函數調用的時候編譯器能夠通過參數來判斷程式是在調用的哪個函數。這也就很自然地解釋了為什麼函數不能通過傳回值不同來重載,因為程式在調用函數時很有可能不關心傳回值,編譯器就無法從代碼中看出程式在調用的是哪個函數了。

(3)      覆寫的函數前必須加關鍵字virtual;

重載和virtual沒有任何瓜葛,加不加都不影響重載的運作。

關于c++的隐藏規則:

我曾經聽說過c++的隐藏規則:

(1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual

關鍵字,基類的函數将被隐藏(注意别與重載混淆)。

(2)如果派生類的函數與基類的函數同名,并且參數也相同,但是基類函數沒有virtual

關鍵字。此時,基類的函數被隐藏(注意别與覆寫混淆)。

                                               ----------引用自《高品質c++/c 程式設計指南》林銳  2001

這裡,林銳博士好像犯了個錯誤。c++并沒有隐藏規則,林銳博士所總結的隐藏規則是他錯誤地了解c++多态性所緻。下面請看林銳博士給出的隐藏規則的例證:

#include <iostream.h>  

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  

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!)  

pb->h(3.14f); // base::h(float) 3.14 (surprise!)  

pd->h(3.14f); // derived::h(float) 3.14  

林銳博士認為bp 和dp 指向同一位址,按理說運作結果應該是相同的,而事實上運作結果不同,是以他把原因歸結為c++的隐藏規則,其實這一觀點是錯的。決定bp和dp調用函數運作結果的不是他們指向的位址,而是他們的指針類型。“隻有在通過基類指針或引用間接指向派生類子類型時多态性才會起作用”(c++ primer 3rdedition)。pb是基類指針,pd是派生類指針,pd的所有函數調用都隻是調用自己的函數,和多态性無關,是以pd的所有函數調用的結果都輸出derived::是完全正常的;pb的函數調用如果有virtual則根據多态性調用派生類的,如果沒有virtual則是正常的靜态函數調用,還是調用基類的,是以有virtual的f函數調用輸出derived::,其它兩個沒有virtual則還是輸出base::很正常啊,nothing

surprise!

是以并沒有所謂的隐藏規則,雖然《高品質c++/c 程式設計指南》是本很不錯的書,可大家不要迷信哦。記住“隻有在通過基類指針或引用間接指向派生類子類型時多态性才會起作用”。

純虛函數:

c++語言為我們提供了一種文法結構,通過它可以指明,一個虛拟函數隻是提供了一個可被子類型改寫的接口。但是,它本身并不能通過虛拟機制被調用。這就是純虛拟函數(pure

virtual function)。 純虛拟函數的聲明如下所示:

class query {  

// 聲明純虛拟函數  

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

// ...  

這裡函數聲明後面緊跟指派0。

包含(或繼承)一個或多個純虛拟函數的類被編譯器識别為抽象基類。試圖建立一個抽象基類的獨立類對象會導緻編譯時刻錯誤。(類似地通過虛拟機制調用純虛函數也是錯誤的)

// query 聲明了純虛拟函數, 我們不能建立獨立的 query 類對象  

// 正确: namequery 是 query 的派生類  

query *pq = new namequery( "nostromo" );  

// 錯誤: new 表達式配置設定 query 對象  

query *pq2 = new query();  

虛析構:

如果一個類用作基類,我們通常需要virtual來修飾它的析構函數,這點很重要。如果基類的析構函數不是虛析構,當我們用delete來釋放基類指針(它其實指向的是派生類的對象執行個體)占用的記憶體的時候,隻有基類的析構函數被調用,而派生類的析構函數不會被調用,這就可能引起記憶體洩露。如果基類的析構函數是虛析構,那麼在delete基類指針時,繼承樹上的析構函數會被自低向上依次調用,即最底層派生類的析構函數會被首先調用,然後一層一層向上直到該指針聲明的類型。

虛繼承:

如果隻知道virtual加在函數前,那對virtual隻了解了一半,virtual還有一個重要用法是virtual public,就是虛拟繼承。虛拟繼承在c++ primer中有詳細的描述,下面稍作修改的闡釋一下:

在預設情況下c++中的繼承是“按值組合”的一種特殊情況。當我們寫

class bear : public zooanimal { ... };

每個bear 類對象都含有其zooanimal 基類子對象的所有非靜态資料成員以及在bear中聲明的非靜态資料成員。類似地當派生類自己也作為一個基類對象時如:

class polarbear : public bear { ... };

則polarbear 類對象含有在polarbear 中聲明的所有非靜态資料成員以及其bear 子對象的所有非靜态資料成員和zooanimal 子對象的所有非靜态資料成員。在單繼承下這種由繼承支援的特殊形式的按值組合提供了最有效的最緊湊的對象表示。在多繼承下當一個基類在派生層次中出現多次時就會有問題最主要的實際例子是iostream 類層次結構。ostream 和istream 類都從抽象ios 基類派生而來,而iostream 類又是從ostream 和istream 派生

class iostream :public istream, public ostream { ... };

預設情況下,每個iostream 類對象含有兩個ios 子對象:在istream 子對象中的執行個體以及在ostream 子對象中的執行個體。這為什麼不好?從效率上而言,iostream隻需要一個執行個體,但我們存儲了ios 子對象的兩個複本,浪費了存儲區。此外,在這一過程中,ios的構造函數被調用了兩次(每個子對象一次)。更嚴重的問題是由于兩個執行個體引起的二義性。例如,任何未限定修飾地通路ios 的成員都将導緻編譯時刻錯誤:到底通路哪個執行個體?如果ostream 和istream 對其ios 子對象的初始化稍稍不同,會怎樣呢?怎樣通過iostream

類保證這一對ios 值的一緻性?在預設的按值組合機制下,真的沒有好辦法可以保證這一點。

c++語言的解決方案是,提供另一種可替代按“引用組合”的繼承機制--虛拟繼承(virtual inheritance)。在虛拟繼承下隻有一個共享的基類子對象被繼承而無論該基類在派生層次中出現多少次。共享的基類子對象被稱為虛拟基類。

       通過用關鍵字virtual 修正,一個基類的聲明可以将它指定為被虛拟派生。例如,下列聲明使得zooanimal 成為bear 和raccoon 的虛拟基類:

// 這裡關鍵字 public 和 virtual的順序不重要

class bear : public virtual zooanimal { ... };

class raccoon : virtual public zooanimal { ... };

虛拟派生不是基類本身的一個顯式特性,而是它與派生類的關系。如前面所說明的,虛拟繼承提供了“按引用組合”。也就是說,對于子對象及其非靜态成員的通路是間接進行的。這使得在多繼承情況下,把多個虛拟基類子對象組合成派生類中的一個共享執行個體,進而提供了必要的靈活性。同時,即使一個基類是虛拟的,我們仍然可以通過該基類類型的指針或引用,來操縱派生類的對象。

繼續閱讀