[收集]c++抽象類、純虛函數以及巧用純虛析構函數實現接口類

在Java、C#中有關鍵詞abstract指明抽象函數、抽象類,但是在C++中沒有這個關鍵詞,很顯然,在C++也會需要只需要在基類聲明某函數的情況,而不需要寫具體的實現,那C++中是如何實現這一功能的,答案是純虛函數 含有純虛函數的類是抽象類,不能生成對象,只能派生。他派生的類的純虛函數沒有被改寫,那麼它的派生類還是個抽象類。定義純虛函數就是爲了讓基類不可實例化化意義。

一.  純虛函數

在許多情況下,在基類中不能給出有意義的虛函數定義,這時可以把它說明成純虛函數,把它的定義留給派生類來做。定義純虛函數的一般形式爲:

class 類名{

 virtual 返回值類型 函數名(參數表)= 0;  // 後面的"= 0"是必須的,否則,就成虛函數了

};

純虛函數是一個在基類中說明的虛函數,它在基類中沒有定義,要求任何派生類都定義自己的版本。純虛函數爲各派生類提供一個公共界面。

從基類繼承來的純虛函數,在派生類中仍是虛函數。

二. 抽象類

1. 如果一個類中至少有一個純虛函數,那麼這個類被稱爲抽象類(abstract class)。

抽象類中不僅包括純虛函數,也可包括虛函數。抽象類中的純虛函數可能是在抽象類中定義的,也可能是從它的抽象基類中繼承下來且重定義的。

2. 抽象類特點,即抽象類必須用作派生其他類的基類,而不能用於直接創建對象實例。

一個抽象類不可以用來創建對象,只能用來爲派生類提供一個接口規範,派生類中必須重載基類中的純虛函數,否則它仍將被看作一個抽象類。

3. 在effective c++上中提到,純虛函數可以被實現(定義)(既然是純虛函數,爲什麼還可以被實現呢?這樣做有什麼好處呢?下文中“巧用純虛析構函數實現接口類”中將說明這一功能的目的。),但是,不能創建對象實例,這也體現了抽象類的概念。

三. 虛析構函數

虛析構函數: 在析構函數前面加上關鍵字virtual進行說明,稱該析構函數爲虛析構函數。雖然構造函數不能被聲明爲虛函數,但析構函數可以被聲明爲虛函數

一般來說,如果一個類中定義了虛函數, 析構函數也應該定義爲虛析構函數。

例如:

class B

{

 virtual ~B();  //虛析構函數

  …

};

下面介紹一些實例:

  1. #include <stdio.h>  
  2.   
  3. class Animal  
  4. {  
  5. public:  
  6.      Animal()   //構造函數不能被聲明爲虛函數  
  7.      {  
  8.     printf(" Animal construct! \n");  
  9.      }  
  10.      virtual void shout() = 0;  
  11.      virtual void impl() = 0;  
  12.      virtual ~Animal() {printf(" Animal destory! \n");};   // 虛析構函數  
  13. };  
  14.   
  15.   
  16. void Animal::impl()        // 純虛函數也可以被實現。  
  17. {  
  18.      printf(" Animal: I can be implement! \n");  
  19. }  
  20.   
  21.   
  22. class Dog: public Animal  
  23. {  
  24. public:  
  25.      Dog()  
  26.      {  
  27.     printf(" Dog construct! \n");  
  28.      }  
  29.      virtual void shout() // 必須要被實現,即使函數體是空的  
  30.      {  
  31.          printf(" Dog: wang! wang! wang! \n");  
  32.      }  
  33.      virtual void impl()  
  34.      {  
  35.          printf(" Dog: implement of Dog!  \n");  
  36.      }  
  37.      virtual ~Dog() {printf(" Dog destory! \n");};   // 虛析構函數  
  38. };  
  39.   
  40.   
  41. class Cat: public Animal  
  42. {  
  43. public:  
  44.      Cat()  
  45.      {  
  46.     printf(" Cat construct! \n");  
  47.      }  
  48.      virtual void shout() // 必須要被實現,即使函數體是空的  
  49.      {  
  50.          printf(" Cat: miao! miao! miao! \n");  
  51.      }  
  52.       
  53.      virtual void impl()  
  54.      {  
  55.          printf(" Cat: implement of Cat!  \n");  
  56.      }  
  57.      virtual ~Cat() {printf(" Cat destory! \n");};   // 虛析構函數  
  58. };  
  59.   
  60.   
  61. /* 
  62. Animal f()  // error, 抽象類不能作爲返回類型 
  63. { 
  64.        
  65. } 
  66.  
  67. void display( Animal a) //error, 抽象類不能作爲參數類型 
  68. { 
  69.      
  70. } 
  71. */  
  72.   
  73.   
  74. //ok,可以聲明抽象類的引用  
  75. Animal &display(Animal &a)  
  76. {  
  77.        Dog d;  
  78.        Animal &p = d;  
  79.        return p;  
  80.         
  81. }  
  82.   
  83. void test_func()  
  84. {  
  85.      //Animal a;  // error: 抽象類不能建立對象  
  86.       
  87.     Dog dog;   //ok,可以聲明抽象類的指針  
  88.     Cat cat;   //ok,可以聲明抽象類的指針  
  89.      
  90.     printf("\n");  
  91.   
  92.     Animal *animal = &dog;  
  93.     animal->shout();  
  94.     animal->impl();  
  95.      
  96.     printf("\n");  
  97.      
  98.     animal = &cat;  
  99.     animal->shout();  
  100.     animal->impl();  
  101.      
  102.     printf("\n");  
  103. }  
  104.   
  105. int main()  
  106. {  
  107.     test_func();  
  108.   
  109.     while(1);    
  110. }  
  111.   
  112.   
  113. //result:  
  114. /* 
  115. Animal construct! 
  116. Dog construct! 
  117. Animal construct! 
  118. Cat construct! 
  119.  
  120. Dog: wang! wang! wang! 
  121. Dog: implement of Dog! 
  122.  
  123. Cat: miao! miao! miao! 
  124. Cat: implement of Cat! 
  125.  
  126. Cat destory! 
  127. Animal destory! 
  128. Dog destory! 
  129. Animal destory! 
  130. */  
  131. (YC:代碼已調試無誤)  


四. 巧用純虛析構函數實現接口類
c++不像java一樣有純接口類的語法,但我們可以通過一些手段實現相同的功能。

(1)能不能用“protected”實現接口類?

看如下代碼:

  1. #include <stdio.h>  
  2.   
  3.   
  4. class A  
  5. {  
  6. protected:  
  7.     virtual ~A()  
  8.     {  
  9.         printf(" A: 析構函數  \n");  
  10.     }  
  11. };  
  12. class B : public A  
  13. {  
  14. public:  
  15.     virtual ~B()  
  16.     {  
  17.         printf(" B: 析構函數  \n");  
  18.     }  
  19. };  
  20. int _tmain(int argc, _TCHAR* argv[])  
  21. {  
  22.     //A* p1 = new A;              //error:[1]有問題  
  23.     //delete p1;  
  24.   
  25.   
  26.     B* p2 = new B;           //ok:[2]沒問題,輸出結果爲:  
  27.     delete p2;               /* B: 析構函數 
  28.                                         A: 析構函數*/(注意此處還是會調用A的析構函數的,不過編譯沒問題)  
  29.                    
  30.     //A* p3 = new B;  
  31.     //delete p3;                 //error:[3] 有問題  
  32.   
  33.   
  34.     return 0;  
  35. }  

通過在類中,將類的構造函數或者析構函數申明成protected ,可以有效防止類被實例話,要說實用的話,構造函數是protected更有用,肯定能保證類不會被實例化,而如果析構函數是protected的話,構造函數不是protected的話,還可能存在編譯通過的漏洞,如下:

Case1:

  1. class A  
  2. {  
  3. protected:  
  4.     A()  
  5.     {  
  6.         printf(" A: A()  \n");  
  7.     }  
  8. };  
  9.   
  10. int _tmain(int argc, _TCHAR* argv[])  
  11. {  
  12.     A* p1 = new A;                //編譯不通過,無法訪問protected構造函數  
  13.     delete p1;  
  14.   
  15.     return 0;  
  16. }  

Case2:

  1. class A  
  2. {  
  3. protected:  
  4.     ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9.   
  10. int _tmain(int argc, _TCHAR* argv[])  
  11. {  
  12.     A* p1 = new A;                //編譯通過,此時因爲僅僅是用到了A的構造函數,還不需要它的析構函數  
  13.     return 0;  
  14. }  
  15.   
  16. (附:如果將main中改爲:  
  17. int _tmain(int argc, _TCHAR* argv[])  
  18. {  
  19.     A a;  
  20.     return 0;  
  21. }  
  22. 則編譯出錯,提示無法訪問protected成員A::~A().兩種情況出現差異的原因是什麼?  
  23.   
  24. )  

Case3:

  1. class A  
  2. {  
  3. protected:  
  4.     ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9. int _tmain(int argc, _TCHAR* argv[])  
  10. {  
  11.     A* p1 = new A;                  
  12.     delete p1;                //編譯失敗,因爲編譯器發現A的析構函數是protected  
  13.     return 0;  
  14. }  


所以,一種可行的辦法貌似是這樣的:

  1. class A  
  2. {  
  3. protected:  
  4.     virtual ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9.   
  10. class B : public A  
  11. {  
  12. };  
  13.   
  14. int _tmain(int argc, _TCHAR* argv[])  
  15. {  
  16.     B* p =new B;       //ok:這種情況下確實是可行的(YC:仔細看會發現這種情況同“(1)看如下代碼”下面的代碼中ok的情況相同)  
  17.     delete  p;  
  18.     return 0;  
  19. }  

由於B public繼承自A,所以其可以完全訪問A的構造或析構函數,但是:

  1. int _tmain(int argc, _TCHAR* argv[])  
  2. {  
  3.     A* p =new B;  
  4.     delete  p;                //error:由於p變成指向A的指針,字面上編譯器需要知道A的析構函數,然後A的析構函數又是protected  
  5.     return 0;  
  6. }  

即便像這樣B顯示重載了A的析構函數:

  1. class A  
  2. {  
  3. protected:  
  4.     virtual ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9. class B : public A  
  10. {  
  11. public:  
  12.     virtual ~B()  
  13.     {  
  14.         printf(" B: ~B()  \n");  
  15.     }  
  16. };  
  17. int _tmain(int argc, _TCHAR* argv[])  
  18. {  
  19.     A* p =new B;  
  20.     delete  p;        //error:也還是不行,因爲重載是運行時的事情,在編譯時編譯器就認定了A的析構函數,結果無法訪問  
  21.     return 0  
  22. }  

小結:

貌似用protected這樣的方法並不是很恰當,雖然在遵守一定規則的情況下確實有他的實用價值,但並不是很通用

(2)應該怎樣實現接口類?

其實上面protected的思路是對的,無非是讓父類無法實例化,那麼爲了讓父類無法實例化,其實還有一個方法,使用純虛函數

  1. class A  
  2. {  
  3. public:            //這裏就不用protected了  
  4.     virtual ~A() = 0;  
  5. };  
  6. class B : public A  
  7. {  
  8. };  
  9. int _tmain(int argc, _TCHAR* argv[])  
  10. {  
  11.     B* p =new B;  
  12.     delete  p;      //編譯ok,鏈接error  
  13.     return 0;  
  14. }  

這樣寫貌似不錯,以往大家都把類中的一般成員函數寫成純虛的,這次將析構函數寫成純虛的,更加增加通用性,編譯也通過了,但就是在鏈接的時候出問題,報錯說找不到A的析構函數的實現,很顯然嘛,因爲A的析構是純虛的嘛。

那麼如何修改上述代碼可以達到既可以去除上述error,又可以讓基類不能被實例化呢?如下所示:

  1. class A  
  2. {  
  3. public:                 //這裏就不用protected了  
  4.     virtual ~A() = 0                //它雖然是個純虛函數,但是也可以被實現  
  5.     {                               //這個語法很好很強大(完全是爲了實現其接口類而弄的語法吧)  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9. class B : public A  
  10. {  
  11. };  
  12. int _tmain(int argc, _TCHAR* argv[])  
  13. {  
  14.     B* p =new B;  
  15.     delete  p;  
  16.     A* p2 =new B;  
  17.     delete  p2;            //不用擔心編譯器報錯了,因爲此時A的析構函數是public  
  18.     return 0;  
  19. }  
  20. //result:  
  21. /* 
  22.  A: ~A() 
  23.  A: ~A() 
  24. */   

如此終於大功告成了,注意,不能將構造函數替代上面的析構函數的用法,因爲構造函數是不允許作爲虛函數的

補充:以上那個語法就真的只是爲了這種情況而存在的,因爲一般我們在虛類中申明的接口:

virtual foo()= 0;

virtual foo()= 0 {}

這兩種寫法是完全沒有區別的純虛函數的默認實現,僅僅在它是析構函數中才有意義!!!

所以可以說,老外是完全爲了這一個目的而發明了這種語法...

最終的接口類

  1. classInterface  
  2. {  
  3. public:         
  4.     virtual ~Interface() = 0 {}  
  5. };  

應該挺完美的了吧
[備註:內容多收集於
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章