一、純虛函數定義
純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在函數原型後加“=0”
二、引入原因:
1、為了方便使用多態特性,我們常常需要在基類中定義虛擬函數。
2、在很多情況下,基類本身生成對象是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。
為了解決上述問題,引入了純虛函數的概念,將函數定義為純虛函數(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重載以實現多態性。同時含有純虛擬函數的類稱為抽象類,它不能生成對象。這樣就很好地解決了上述兩個問題。
帶有純虛函數的類稱為抽象類。抽象類是一種特殊的類,它是為了抽象和設計的目的而建立的,它處於繼承層次結構的較上層。抽象類是不能定義對象的,在實際中為了強調一個類是抽象類,可將該類的構造函數說明為保護的訪問控制權限。
抽象類的主要作用是將有關的組織在一個繼承層次結構中,由它來為它們提供一個公共的根,相關的子類是從這個根派生出來的。
抽象類刻畫了一組子類的操作接口的通用語義,這些語義也傳給子類。一般而言,抽象類只描述這組子類共同的操作接口,而完整的實現留給子類。
抽象類只能作為基類來使用,其純虛函數的實現由派生類給出。如果派生類沒有重新定義純虛函數,而派生類只是繼承基類的純虛函數,則這個派生類仍然還是一個 抽象類。如果派生類中給出了基類純虛函數的實現,則該派生類就不再是抽象類了,它是一個可以建立對象的具體類了。
抽象類的規定
(1)抽象類只能用作其他類的基類,不能建立抽象類對象。
(2)抽象類不能用作參數類型、函數返回類型或顯式轉換的類型。
(3)可以定義指向抽象類的指針和引用,此指針可以指向它的派生類,進而實現多態性。
#include<iostream> using namespace std; const double PI=3.14159; class Shapes //抽象類 { protected: int x, y; public: void setvalue(int d, int w=0){x=d;y=w;} virtual void disp()=0;//純虛函數 }; class Square:public Shapes { public: void disp(){ cout<<"矩形面積:"<<x*y<<endl; } }; class Circle:public Shapes{ public: void disp(){ cout<<"圓面積:"<<PI*x*x<<endl; } }; int main() { Shapes *ptr[2]; //定義對象指針數組 Square s1; Circle c1; ptr[0] = &s1; ptr[0]->setvalue(10, 5); ptr[0]->disp(); ptr[1] = &c1; ptr[1]->setvalue(10); ptr[1]->disp(); return 0; }
三、相似概念:
1、多態性
指相同對象收到不同消息或不同對象收到相同消息時產生不同的實現動作。C++支持兩種多態性:編譯時多態性,運行時多態性。
a.編譯時多態性: 通過函數重載和運算符重載來實現的。
b 運行時多態性:通過繼承和虛函數來實現的。
2、虛函數
虛函數是在基類中被聲明為virtual,並在派生類中重新定義的成員函數,可實現成員函數的動態重載
純虛函數的聲明有著特殊的語法格式:virtual 返回值類型成員函數名(參數表)=0;
請注意,純虛函數應該只有聲明,沒有具體的定義,即使給出了純虛函數的定義也會被編譯器忽略。
3、抽象類
包含純虛函數的類稱為抽象類。由於抽象類包含了沒有定義的純虛函數,所以不能定義抽象類的對象。
在C++中,我們可以把只能用於被繼承而不能直接創建對象的類設置為抽象類(Abstract Class)。
之所以要存在抽象類,最主要是因為它具有不確定因素。我們把那些類中的確存在,但是在父類中無法確定具體實現的成員函數稱為純虛函數。純虛函數是一種特殊的虛函數,它只有聲明,沒有具體的定義。抽象類中至少存在一個純虛函數;存在純虛函數的類一定是抽象類。存在純虛函數是成為抽象類的充要條件。
//基類: class A { public: A(); void f1(); virtual void f2(); virtual void f3()=0; virtual ~A(); }; //子類: class B : public A { public: B(); void f1(); void f2(); void f3(); virtual ~B(); }; //主函數: int main(int argc, char* argv[]) { A *m_j=new B(); m_j->f1(); m_j->f2(); m_j->f3(); delete m_j; return 0; } /* f1()是一個普通的重載. 調用m_j->f1();會去調用A類中的f1(),它是在我們寫好代碼的時候就會定好的.因為f1()不是虛函數,不會動態綁定 也就是根據它是由A類定義的,這樣就調用這個類的函數. f2()是虛函數. 調用m_j->f2();會調用m_j中保存的對象中,對應的這個函數.這是由於new的B對象. f3()與f2()一樣,只是在基類中不需要寫函數實現. */
虛函數和純虛函數有以下所示方面的區別。
(1)類裡如果聲明了虛函數,這個函數是實現的,哪怕是空實現,它的作用就是為了能讓這個函數在它的子類裡面可以被覆蓋,這樣的話,這樣編譯器就可以使用後期綁定來達到多態了。純虛函數只是一個接口,是個函數的聲明而已,它要留到子類裡去實現。
(2)虛函數在子類裡面也可以不重載的;但純虛函數必須在子類去實現,這就像Java的接口一樣。通常把很多函數加上virtual,是一個好的習慣,雖然犧牲了一些性能,但是增加了面向對象的多態性,因為很難預料到父類裡面的這個函數不在子類裡面不去修改它的實現。
(4)帶純虛函數的類叫虛基類,這種基類不能直接生成對象,而只有被繼承,並重寫其虛函數後,才能使用。這樣的類也叫抽象類。抽象類和大家口頭常說的虛基類還是有區別的,在C#中用abstract定義抽象類,而在C++中有抽象類的概念,但是沒有這個關鍵字。抽象類被繼承後,子類可以繼續是抽象類,也可以是普通類,而虛基類,是含有純虛函數的類,它如果被繼承,那麼子類就必須實現虛基類裡面的所有純虛函數,其子類不能是抽象類。
三、抽象類和接口的區別:
1.類是對對象的抽象,可以把抽象類理解為把類當作對象,抽象成的類叫做抽象類.而接口只是一個行為的規范或規定,微軟的自定義接口總是後帶able字段,證明其是表述一類類“我能做。。。”.抽象類更多的是定義在一系列緊密相關的類間,而接口大多數是關系疏松但都實現某一功能的類中.
2.接口基本上不具備繼承的任何具體特點,它僅僅承諾了能夠調用的方法;
3.一個類一次可以實現若干個接口,但是只能擴展一個父類
4.接口可以用於支持回調,而繼承並不具備這個特點.
5.抽象類不能被密封。
6.抽象類實現的具體方法默認為虛的,但實現接口的類中的接口方法卻默認為非虛的,當然您也可以聲明為虛的.
7.(接口)與非抽象類類似,抽象類也必須為在該類的基類列表中列出的接口的所有成員提供它自己的實現。但是,允許抽象類將接口方法映射到抽象方法上。
8.抽象類實現了oop中的一個原則,把可變的與不可變的分離。抽象類和接口就是定義為不可變的,而把可變的座位子類去實現。
9.好的接口定義應該是具有專一功能性的,而不是多功能的,否則造成接口污染。如果一個類只是實現了這個接口的中一個功能,而不得不去實現接口中的其他方法,就叫接口污染。
10.盡量避免使用繼承來實現組建功能,而是使用黑箱復用,即對象組合。因為繼承的層次增多,造成最直接的後果就是當你調用這個類群中某一類,就必須把他們全部加載到棧中!後果可想而知.(結合堆棧原理理解)。同時,有心的朋友可以留意到微軟在構建一個類時,很多時候用到了對象組合的方法。比如asp.net中,Page類,有Server Request等屬性,但其實他們都是某個類的對象。使用Page類的這個對象來調用另外的類的方法和屬性,這個是非常基本的一個設計原則。
11.如果抽象類實現接口,則可以把接口中方法映射到抽象類中作為抽象方法而不必實現,而在抽象類的子類中實現接口中方法.