在C++中由兩種多態性:html
• 編譯時的多態性:經過函數的重載和運算符的重載來實現的ios
• 運行時的多態性:經過類繼承關係和虛函數來實現的程序員
特別注意:算法
a.運行時的多態性是指程序執行前,沒法根據函數名和函數的參數來肯定調用哪個函數,必須在程序執行的過程當中,根據執行的具體狀況來動態地肯定。其目的是追求程序的通用性,創建一種通用的程序函數
b.運行時的多態,簡而言之就是用父類型的指針或引用指向其子類的實例,而後經過父類的指針或引用調用實際子類的成員函數,從而使父類的指針或引用擁有「多種形態」。這是一種泛型技術(如:模版技術、RTTI技術),其目的是使用不變的代碼來實現可變的算法this
示例:spa
1 #include<iostream> 2 using namespace std; 3 class Animal{ //基類 4 public: 5 virtual void eat(){ //虛函數 6 cout<<"Animal eat"<<endl; 7 } 8 virtual void sleep(){ 9 cout<<"Animal sleep"<<endl; 10 } 11 }; 12 13 class Person:public Animal{ //子類1 14 public: 15 void eat(){ 16 cout<<"Person eat"<<endl; 17 } 18 void sleep(){ 19 cout<<"Person sleep"<<endl; 20 } 21 }; 22 23 class Dog:public Animal{ //子類2 24 public: 25 void eat(){ 26 cout<<"Dog eat"<<endl; 27 } 28 void sleep(){ 29 cout<<"Dog sleep"<<endl; 30 } 31 }; 32 33 class Bird:public Animal{ //子類3 34 public: 35 void eat(){ 36 cout<<"Bird eat"<<endl; 37 } 38 void sleep(){ 39 cout<<"Bird sleep"<<endl; 40 } 41 }; 42 43 void func(Animal &a){ //函數,注意參數是基類的引用 44 a.eat(); 45 a.sleep(); 46 } 47 48 int main(){ 49 Person p; //Person類的對象實例 50 Dog d; //Dog類的對象實例 51 Bird b; //Bird類的對象實例 52 func(p); 53 cout<<"-----------分界線---------------"<<endl; 54 func(d); 55 cout<<"-----------分界線---------------"<<endl; 56 func(b); 57 return 0; 58 }
虛函數是一個類的成員函數,它的定義語法以下:3d
語法:virtual 返回值類型 函數名(參數表);
特別注意:指針
a.當一個類的一個成員函數被定義爲虛函數時,則由該類派生出來的全部派生類中,該函數始終保持虛函數的特徵code
b.當在派生類中從新定義虛函數時,沒必要加關鍵字virtual。但從新定義時不只要求函數同名,並且要求函數的參數列表與返回值類型也必須和基類中的虛函數相同,不然編譯器會報錯
c.虛函數能夠在先在類內進行聲明,而後在類外定義。但在類內聲明時須要在返回值類型以前加上關鍵字virtual,在類外定義時則不須要在添加關鍵字virtual
1.派生類中重定義虛函數時,虛函數的函數名必須與其基類中的虛函數的函數名相同,除此以外要求參數列表和函數的返回值類型也必須相同
[特例]:當基類中的虛函數的返回值類型是基類類型的指針時,容許在派生類中重定義該虛函數時將返回值類型改寫爲派生類類型的指針
1 #include<iostream> 2 using namespace std; 3 class Animal{ 4 public: 5 int value; 6 Animal():value(0){} 7 Animal(int v):value(v){} 8 virtual Animal* show(){ //返回值類型是Animal類型的指針 9 cout<<"Animal類中的value值是:"<<value<<endl; 10 return this; 11 } 12 }; 13 14 class Person:public Animal{ 15 public: 16 int value; 17 Person():value(0){} 18 Person(int v):value(v){} 19 Person* show(){ //返回值類型是Person類型的指針 20 cout<<"Person類中的value值是:"<<value<<endl; 21 return this; 22 } 23 }; 24 25 int main(){ 26 Animal animal(10); 27 Person person(20); 28 animal.show(); 29 cout<<"------------分界線----------------"<<endl; 30 person.show(); 31 return 0; 32 }
2.只有類中的成員函數纔有資格成爲虛函數。這是由於虛函數僅適用於有繼承關係的類對象(建議成員函數儘量地設置爲虛函數)
3.類中的靜態成員函數是該類全部對象所共有的,不受限於某個對象個體,所以不能做爲虛函數
4.內聯(inline)函數不能是虛函數,由於內聯函數不能在運行中動態肯定位置。即便虛函數在類的內部定義,可是在編譯的時候系統仍然將它看作是非內聯的
5.類中的析構函數能夠做爲虛函數,但構造函數不能做爲虛函數。這是由於在調用構造函數時對象尚未完成實例化,而調用析構函數時對象已經完成了實例化
[注]:在基類中及其派生類中都動態分配內存空間時,必須把析構函數定義爲虛函數,從而實現「銷燬」對象時的多態性。例如在C++中用new運算符創建臨時對象時,若基類中有析構函數而且同時定義了一個指向該基類的指針變量,但指針變量指向的對象倒是該基類的派生類對象,那麼在程序執行delete操做時,系統只會執行基類中的析構函數,而不會執行派生類中的析構函數,從而形成內存泄漏
1 #include<iostream> 2 using namespace std; 3 class Father{ 4 public: 5 Father()=default; 6 ~Father(){ 7 cout<<"調用Father類的析構函數"<<endl; 8 } 9 }; 10 class Son:public Father{ 11 public: 12 Son()=default; 13 ~Son(){ 14 cout<<"調用Son類的析構函數"<<endl; 15 } 16 }; 17 18 int main(){ 19 Father* ptr=new Son; 20 delete ptr; 21 return 0; 22 }
這是由於這裏的指針本質上指向的實際上是派生類對象中隱藏包含的基類子對象。將基類的析構函數定義爲虛函數能夠解決這個問題:
1 #include<iostream> 2 using namespace std; 3 class Father{ 4 public: 5 Father()=default; 6 virtual ~Father(){ 7 cout<<"調用Father類的析構函數"<<endl; 8 } 9 }; 10 class Son:public Father{ 11 public: 12 Son()=default; 13 ~Son(){ 14 cout<<"調用Son類的析構函數"<<endl; 15 } 16 }; 17 18 int main(){ 19 Father* ptr=new Son; 20 delete ptr; 21 return 0; 22 }
當基類中的析構函數爲虛函數時,不管指針指向的是同一類族中的哪個對象,系統都會採用動態關聯,調用相應的析構函數,對該對象進行清理工做。所以最好將基類中的析構函數聲明爲虛函數,這將使該基類的全部派生類的析構函數自動成爲虛函數
6.使用虛函數會使程序的運行速度下降,這是由於爲了實現多態性,每個派生類中均要保存相應虛函數的入口地址表,函數的調用機制也是間接實現,但程序的通用性會變得更高
7.若是虛函數的定義放在類外,virtual關鍵字只加在函數的聲明的前面,不能再添加在函數定義的前面。正確的定義必須不包括關鍵字virtual
在C++中,虛函數是經過虛表(Virtual Table)來實現的。虛表本質上是一個類的虛函數的地址表,它解決了繼承、覆蓋的問題,保證其容量真實反映實際的函數。而對虛表的利用,每每須要經過指向虛表的指針來實現,在C++中,編譯器必須保證虛表的指針存在於對象實例中最前面的位置(這是爲了保證正確取到虛函數的偏移量),這樣咱們即可以經過對象實例的地址獲得虛表,而後利用指向虛表的指針遍歷虛表中的函數指針,並調用相應的函數
示例:
1 #include<iostream> 2 using namespace std; 3 class Father{ 4 public: 5 virtual void show(){ //虛函數1 6 cout<<"調用Father類的成員方法show()"<<endl; 7 } 8 virtual void func(){ //虛函數2 9 cout<<"調用Father類的成員方法func()"<<endl; 10 } 11 void print(){ //普通成員函數 12 cout<<"調用Father類的成員方法print()"<<endl; 13 } 14 }; 15 16 class Son:public Father{ 17 public: 18 void show(){ //虛函數1 19 cout<<"調用Son類的成員方法show()"<<endl; 20 } 21 void func(){ //虛函數2 22 cout<<"調用Son類的成員方法func()"<<endl; 23 } 24 virtual void list(){ //虛函數3 25 cout<<"調用Son類的成員方法list()"<<endl; 26 } 27 }; 28 29 int main(){ 30 Father father; 31 father.show(); 32 father.func(); 33 father.print(); 34 cout<<"-----------分界線------------"<<endl; 35 Son son; 36 son.show(); 37 son.func(); 38 son.print(); 39 son.list(); 40 return 0; 41 }
圖解說明:
當Father類建立對象father後,其內存分佈大體以下:
Father類中的成員函數print()是普通成員函數,所以其再也不虛表中。
當Son類建立對象son後,其內存分佈大體以下:
如上圖所示,在這個繼承關係中,子類Derive沒有重定義任何父類Base的虛函數,而是在其繼承父類Base的基礎上添加了三個新的虛函數,其代碼主要以下(摘要):
1 class Base{//基類 2 public: 3 virtual void f(); 4 virtual void g(); 5 virtual void h(); 6 }; 7 8 class Derive{ //子類 9 public: 10 virtual void f1(); 11 virtual void g1(); 12 virtual void h1(); 13 };
此時對於子類的實例(Derive d;)來講,其虛表結構大體以下:
從中咱們能夠看出:
• 虛函數按照其聲明順序放於虛表中
• 父類的虛函數在子類的虛函數的前面
如上圖所示,在這個繼承關係中,子類Derive重定義了部分父類Base的虛函數,其代碼主要以下(摘要):
1 class Base{//基類 2 public: 3 virtual void f(); 4 virtual void g(); 5 virtual void h(); 6 }; 7 8 class Derive{ //子類 9 public: 10 void f(); //重定義基類中的虛函數 11 virtual void g1(); 12 virtual void h1(); 13 };
此時對於子類的實例(Derive d;)來講,其虛表結構大體以下:
從中咱們能夠看出:
• 派生類中重定義的虛函數(如void f())會被放到虛表中原來父類虛函數的位置
• 沒有被重定義的虛函數保持原樣
[注]:正因如此,當程序執行語句:
1 Base *b=new Derive(); 2 b->f();
因爲虛表中Base::f()的位置已經被Derive::f()函數地址所取代,所以指針b此時調用的函數f()是Derive::f(),而不是Base::f()
如上圖所示,在這個繼承關係中,子類Derive沒有重定義任何父類中的虛函數,而是在其繼承全部父類的基礎上添加了兩個新的虛函數,其代碼主要以下(摘要):
1 class Base1{//基類1 2 public: 3 virtual void f(); 4 virtual void g(); 5 virtual void h(); 6 }; 7 8 class Base2{//基類2 9 public: 10 virtual void f(); 11 virtual void g(); 12 virtual void h(); 13 }; 14 15 class Base3{//基類3 16 public: 17 virtual void f(); 18 virtual void g(); 19 virtual void h(); 20 }; 21 22 class Derive{ //子類 23 public: 24 virtual void f1(); 25 virtual void g1(); 26 };
此時對於子類的實例(Derive d;)來講,其虛表結構大體以下:
從中咱們能夠看出:
• 每個父類都有本身的虛表
• 子類中新增長的虛函數會被添加在第一個父類的虛表的後面(所謂的第一個父類時按照聲明的順序來判斷的),這樣作的目的是爲了使不一樣父類類型的指針在指向同一個子類的實例時都可以調用到實際的函數
如上圖所示,在這個繼承關係中,子類Derive重定義了部分父類中的虛函數,其代碼主要以下(摘要):
1 class Base1{//基類1 2 public: 3 virtual void f(); 4 virtual void g(); 5 virtual void h(); 6 }; 7 8 class Base2{//基類2 9 public: 10 virtual void f(); 11 virtual void g(); 12 virtual void h(); 13 }; 14 15 class Base3{//基類3 16 public: 17 virtual void f(); 18 virtual void g(); 19 virtual void h(); 20 }; 21 22 class Derive{ //子類 23 public: 24 void f(); 25 virtual void g1(); 26 };
此時對於子類的實例(Derive d;)來講,其虛表結構大體以下:
從中咱們能夠看到:
• 派生類中重定義虛函數(如void f())時,其全部父類的虛表中的相應位置都會被替換
• 沒有被重定義的虛函數保持原樣
純虛函數(pure virtual function)是指被標明爲不具體實現的虛擬成員函數。一般狀況下,純虛函數經常使用在這種狀況:定義一個基類時,基類中虛函數的具體實現因爲必須依賴派生類的具體狀況從而沒法在基類中確切定義,此時能夠把這個虛函數定義爲純虛函數
語法:virtual 返回值類型 函數名(參數表)=0;
• 含有純虛函數的基類是不能用來定義對象的。純虛函數沒有實現部分,不能產生對象,因此含有純虛函數的類時抽象類
• 定義純虛函數時,不須要定義函數的實現部分(由於沒有意義,即便定義了函數的實現部分,編譯器也不會對這部分代碼進行編譯)
• 「=0」代表程序員將不定義該函數,函數聲明是爲派生類保留一個位置。「=0」的本質是將指向函數體的指針定位NULL
• 派生類必須重定義基類中的全部純虛函數,少一個都不行,不然派生類中因爲仍包含純虛函數(從基類中繼承而來),系統會仍將該派生類當成一個抽象類而不容許其實例化對象
示例:
1 #include<iostream> 2 using namespace std; 3 class Animal{ //基類,抽象類 4 public: 5 virtual void eat()=0; //純虛函數 6 virtual void sleep()=0; 7 }; 8 9 class Person:public Animal{ //子類1 10 public: 11 void eat(){ 12 cout<<"Person eat"<<endl; 13 } 14 void sleep(){ 15 cout<<"Person sleep"<<endl; 16 } 17 }; 18 19 class Dog:public Animal{ //子類2 20 public: 21 void eat(){ 22 cout<<"Dog eat"<<endl; 23 } 24 void sleep(){ 25 cout<<"Dog eat"<<endl; 26 } 27 }; 28 29 void func(Animal &a){ 30 a.eat(); 31 a.sleep(); 32 } 33 int main(){ 34 Person person; 35 func(person); 36 cout<<"------分界線-----------"<<endl; 37 Dog dog; 38 func(dog); 39 return 0; 40 }