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

[cpp]  view plain copy
  1. class Base  
  2. {  
  3. public:Base(){}  
  4. public:  
  5.        virtual void print(){cout<<"Base";}  
  6. };  
  7.    
  8. class Derived:public Base  
  9. {  
  10. public:Derived(){}  
  11. public:  
  12.        void print(){cout<<"Derived";}  
  13. };  
  14.    
  15. int main()  
  16. {  
  17.        Base *point=new Derived();  
  18.        point->print();  
  19. }   
//---------------------------------------------------------
Output:
Derived
//---------------------------------------------------------
這也許會令人聯想到函數的重載,但稍加對比就會發現二者是徹底不一樣的:
(1)       重載的幾個函數必須在同一個類中;
覆蓋的函數必須在有繼承關係的不一樣的類中
(2)       覆蓋的幾個函數必須函數名、參數、返回值都相同;
重載的函數必須函數名相同,參數不一樣。參數不一樣的目的就是爲了在函數調用的時候編譯器可以經過參數來判斷程序是在調用的哪一個函數。這也就很天然地解釋了爲何函數不能經過返回值不一樣來重載,由於程序在調用函數時頗有可能不關心返回值,編譯器就沒法從代碼中看出程序在調用的是哪一個函數了。
(3)       覆蓋的函數前必須加關鍵字Virtual;
重載和Virtual沒有任何瓜葛,加不加都不影響重載的運做。
 
關於C++的隱藏規則:
我曾經據說過C++的隱藏規則:
(1)若是派生類的函數與基類的函數同名,可是參數不一樣。此時,不論有無virtual
關鍵字,基類的函數將被隱藏(注意別與重載混淆)。
(2)若是派生類的函數與基類的函數同名,而且參數也相同,可是基類函數沒有virtual
關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。
                                               ----------引用自《高質量C++/C 編程指南》林銳  2001
這裏,林銳博士好像犯了個錯誤。C++並無隱藏規則,林銳博士所總結的隱藏規則是他錯誤地理解C++多態性所致。下面請看林銳博士給出的隱藏規則的例證:
[cpp]  view plain copy
  1. #include <iostream.h>  
  2. class Base  
  3. {  
  4. public:  
  5. virtual void f(float x){ cout << "Base::f(float) " << x << endl; }  
  6. void g(float x){ cout << "Base::g(float) " << x << endl; }  
  7. void h(float x){ cout << "Base::h(float) " << x << endl; }  
  8. };  
  9.    
  10. class Derived : public Base  
  11. {  
  12. public:  
  13. virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }  
  14. void g(int x){ cout << "Derived::g(int) " << x << endl; }  
  15. void h(float x){ cout << "Derived::h(float) " << x << endl; }  
  16. };  
  17.    
  18. void main(void)  
  19. {  
  20. Derived d;  
  21. Base *pb = &d;  
  22. Derived *pd = &d;  
  23. // Good : behavior depends solely on type of the object  
  24. pb->f(3.14f); // Derived::f(float) 3.14  
  25. pd->f(3.14f); // Derived::f(float) 3.14  
  26. // Bad : behavior depends on type of the pointer  
  27. pb->g(3.14f); // Base::g(float) 3.14  
  28. pd->g(3.14f); // Derived::g(int) 3 (surprise!)  
  29. // Bad : behavior depends on type of the pointer  
  30. pb->h(3.14f); // Base::h(float) 3.14 (surprise!)  
  31. pd->h(3.14f); // Derived::h(float) 3.14  
  32. }   
林銳博士認爲bp 和dp 指向同一地址,按理說運行結果應該是相同的,而事實上運行結果不一樣,因此他把緣由歸結爲C++的隱藏規則,其實這一觀點是錯的。決定bp和dp調用函數運行結果的不是他們指向的地址,而是他們的指針類型。「只有在經過基類指針或引用間接指向派生類子類型時多態性纔會起做用」(C++ Primer 3 rdEdition)。pb是基類指針,pd是派生類指針,pd的全部函數調用都只是調用本身的函數,和多態性無關,因此pd的全部函數調用的結果都輸出Derived::是徹底正常的;pb的函數調用若是有virtual則根據多態性調用派生類的,若是沒有virtual則是正常的靜態函數調用,仍是調用基類的,因此有virtual的f函數調用輸出Derived::,其它兩個沒有virtual則仍是輸出Base::很正常啊,nothing surprise!
因此並無所謂的隱藏規則,雖然《高質量C++/C 編程指南》是本很不錯的書,可你們不要迷信哦。記住「只有在經過基類指針或引用間接指向派生類子類型時多態性纔會起做用」。
 
純虛函數:
C++語言爲咱們提供了一種語法結構,經過它能夠指明,一個虛擬函數只是提供了一個可被子類型改寫的接口。可是,它自己並不能經過虛擬機制被調用。這就是純虛擬函數(pure
virtual function)。 純虛擬函數的聲明以下所示:
[cpp]  view plain copy
  1. class Query {  
  2. public:  
  3. // 聲明純虛擬函數  
  4. virtual ostream& print( ostream&=cout ) const = 0;  
  5. // ...  
  6. };  
這裏函數聲明後面緊跟賦值0。
包含(或繼承)一個或多個純虛擬函數的類被編譯器識別爲抽象基類。試圖建立一個抽象基類的獨立類對象會致使編譯時刻錯誤。(相似地經過虛擬機制調用純虛函數也是錯誤的)
[cpp]  view plain copy
  1. // Query 聲明瞭純虛擬函數, 咱們不能建立獨立的 Query 類對象  
  2. // 正確: NameQuery 是 Query 的派生類  
  3. Query *pq = new NameQuery( "Nostromo" );  
  4. // 錯誤: new 表達式分配 Query 對象  
  5. 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 { ... };
虛擬派生不是基類自己的一個顯式特性,而是它與派生類的關係。如前面所說明的,虛擬繼承提供了「按引用組合」。也就是說,對於子對象及其非靜態成員的訪問是間接進行的。這使得在多繼承狀況下,把多個虛擬基類子對象組合成派生類中的一個共享實例,從而提供了必要的靈活性。同時,即便一個基類是虛擬的,咱們仍然能夠經過該基類類型的指針或引用,來操縱派生類的對象。

轉自 http://blog.csdn.net/ring0hx/article/details/1605254 編程

相關文章
相關標籤/搜索