C++ Virtual詳解

pamxy發表於2013-06-30

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

    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)

  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++多型性所致。下面請看林銳博士給出的隱藏規則的例證:
  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 3rdEdition)。pb是基類指標,pd是派生類指標,pd的所有函式呼叫都只是呼叫自己的函式,和多型性無關,所以pd的所有函式呼叫的結果都輸出Derived::是完全正常的;pb的函式呼叫如果有virtual則根據多型性呼叫派生類的,如果沒有virtual則是正常的靜態函式呼叫,還是呼叫基類的,所以有virtual的f函式呼叫輸出Derived::,其它兩個沒有virtual則還是輸出Base::很正常啊,nothing surprise!
所以並沒有所謂的隱藏規則,雖然《高質量C++/C 程式設計指南》是本很不錯的書,可大家不要迷信哦。記住“只有在通過基類指標或引用間接指向派生類子型別時多型性才會起作用”。
 
純虛擬函式:
C++語言為我們提供了一種語法結構,通過它可以指明,一個虛擬函式只是提供了一個可被子型別改寫的介面。但是,它本身並不能通過虛擬機器制被呼叫。這就是純虛擬函式(pure
virtual function)。 純虛擬函式的宣告如下所示:
  1. class Query {  
  2. public:  
  3. // 宣告純虛擬函式  
  4. virtual ostream& print( ostream&=cout ) const = 0;  
  5. // ...  
  6. };  
這裡函式宣告後面緊跟賦值0。
包含(或繼承)一個或多個純虛擬函式的類被編譯器識別為抽象基類。試圖建立一個抽象基類的獨立類物件會導致編譯時刻錯誤。(類似地通過虛擬機器制呼叫純虛擬函式也是錯誤的)
  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 { ... };
虛擬派生不是基類本身的一個顯式特性,而是它與派生類的關係。如前面所說明的,虛擬繼承提供了“按引用組合”。也就是說,對於子物件及其非靜態成員的訪問是間接進行的。這使得在多繼承情況下,把多個虛擬基類子物件組合成派生類中的一個共享例項,從而提供了必要的靈活性。同時,即使一個基類是虛擬的,我們仍然可以通過該基類型別的指標或引用,來操縱派生類的物件。

相關文章