1.函數重載
由靜態聯編支援的多態性稱為編譯時間的多態性或靜態多態性,也就是說,確定同名操作的具體操作對象的過程是在編譯過程中完成的。在C++中,可以用函數重載和運算子多載來實現編譯時間的多態性。
2.虛函數
由動態聯編支援的多態性稱為運行時的多太性或動態多態性,也就是說,確定同名操作的具體操作對象的過程是在運行過程中完成的。在C++中,可以用虛函數來實現運行時的多態性。
2.1.虛函數定義
虛函數的定義是在基類中進行的,即把基類中需要定義為虛函數的成員函式宣告為virtual。當基類中的某個成員函數被聲明為虛函數後,就可以在衍生類別中被重新定義。在衍生類別中重定義時,其函數原型,包括傳回型別、函數函數名、參數個數和類型、參數的順序都必須與基類中的原型完全一致。
定義形式為:
virtual <函數類型><函數名>(參數表){函數體}
例如:解決博文多態概念時的問題代碼如下:
#include<iostream>using namespace std;class Animal{ public: void sleep() { cout<<"Animal Sleep"<<endl; } virtual void breathe()//加上virtual { cout<<"Animal Breathe"<<endl; } };class Fish:public Animal{ public: void breathe() { cout<<"Fish Bubble"<<endl; } };int main(){ Fish fs; Animal *an=&fs; an->breathe(); system("pause"); }
只是加了一個virtual問題就解決了。輸出結果為:
事實上,當將基類中的成員函數breathe()聲明為virtual即虛函數時,編譯器在編譯的時候發現Animal類中有虛函數,此時編譯器會為每個包含虛函數的類建立一個虛表,該表是一個一維數組,在這個數組中存放每個虛函數的地址。上述代碼中,Animal類和Fish類都包含了一個虛函數breathe(),因此編譯器會為這兩個類分別建立一個虛表,如所示。
類Animal和類Fish的虛表
在上述代碼中,當Fish類的fs物件建構完畢後,其內部的虛表指標也就被初始化為指向Fish類的虛表。在轉換後,調用an->breathe(),由於an實際指向的是Fish類的對象,該對象內部的虛表指標指向的是Fish類的虛表,因此最終調用的是Fish類的breathe()函數。
使用派生對象指標時應注意的問題:
1.聲明為指向基類對象的指標可以指向它的公有衍生類別的對象,但不允許指向它的私人衍生類別的對象。
2.允許聲明為指向基類對象的指標指向它的公有衍生類別的對象,但不允許將一個聲明為指向衍生類別對象的指標指向基類的對象。
3.聲明為指向基類對象的指標,當其指向它的公有衍生類別的對象時(滿足第1條),只能直接存取衍生類別中從基類繼承下來的成員,不能直接存取公有衍生類別中定義的成員。要想訪問其公有衍生類別中的成員,可將基類指標用顯式類型轉換方式轉換為衍生類別指標。例如:
#include<iostream>using namespace std;class B{ public: void vf() { cout<<"This is class B"<<endl; } };class D:public B{ public: void vf() { cout<<"This is class D"<<endl; } };int main(){ B b,*pb; D d,*pd; pb= &b; pb->vf(); pb=&d;//滿足第1第2條。 pb->vf(); // pd=&b;//違背第2條。invalid conversion from `B*' to `D*' // pd->vf(); pd=(D*)&b;//滿足第3條。 pd->vf(); pd=&d; pd->vf(); system("pause"); }
如果把vf改成virtual類型的,有:
#include<iostream>using namespace std;class B{ public: virtual void vf() { cout<<"This is class B"<<endl; } };class D:public B{ public: void vf() { cout<<"This is class D"<<endl; } };int main(){ B b,*pb; D d,*pd; pb= &b; pb->vf(); pb=&d;//滿足第1第2條。 pb->vf(); //pd=&b;//違背第2條。invalid conversion from `B*' to `D*' //pd->vf(); pd=(D*)&b;//滿足第3條。 這裡是多態。 pd->vf(); pd=&d; pd->vf(); system("pause"); }
虛函數可以很好的實現多態,在使用虛函數時應注意如下問題:
1.虛函數的聲明只能出現在類函數原型的聲明中,不出現在函數體實現的時候,而且,基類中只有保護成員或公有成員才能被聲明為虛函數。
2.在衍生類別中重新定義虛函數時,關鍵字virtual可以寫也可以不寫,但是在容易引起混亂時,應該寫上該關鍵字。
3.動態聯編只能通過成員函數來調用或者通過指標、引用來訪問虛函數,如果以對象名的形式來訪問虛函數,將採用靜態聯編。
在衍生類別中重新定義基類中的虛函數,是函數重載的另一種形式,但它與函數重載 又有如下區別:
1.一般的函數重載,要求其函數的參數或參數類型必須有所不同,函數的傳回型別也可以不同。
2.重載一個虛函數時,要求函數名、傳回型別、參數個數、參數類型和參數順序都必須與基類中的虛函數完全一致。
3.如果僅傳回型別不同,其餘相同,則系統會給出錯誤資訊。
4.如果函數名相同,而參數個數、參數的類型或參數的順序不同,系統認為是普通的函數重載,虛函數的特性將被丟掉。
2.2 多級繼承和虛函數
原生態多級繼承例子:
#include<iostream>using namespace std;class B{ public: int ms; public: void set() { cout<<"Base set"<<endl; } };class D:public B{ public: void set() { cout<<"D set"<<endl; } };class DD:public D{ };class DDD:public DD{ public: void set() { cout<<"DDD set"<<endl; } };class DDDD:public DDD{ };class DDDDD:public DDDD{ };int main(){ DD d; d.set(); DDDD dddd; dddd.set(); DDDDD ddddd; ddddd.set(); system("pause"); }
上述代碼結果說明,如果類中沒有對應的成員函數,則會向上一級一級找,直到出現所需要的成員函數。如上面的set。d.set在DD類中無set類,就向上找,找到離自己最近的父類的成員函數。
多級繼承可以看做是多個單繼承的組合,多級繼承的虛函數與單繼承的虛函數的調用相同,一個虛函數無論被繼承多少次,仍保持其虛函數的特性,與繼承的次數無關。例如:
#include<iostream>using namespace std;class Base{ public: virtual ~Base(){} virtual void func() { cout<<"Base func"<<endl; } };class Derived1:public Base{ void func() { cout<<"Derived1 func"<<endl; } };class Derived2:public Derived1{ void func() { cout<<"Derived2 func"<<endl; } };void test(Base &b)//引用方式 { b.func(); }int main(){ Base b; Derived1 d1; Derived2 d2; test(b); test(d1); test(d2); system("pause"); }
上述代碼中在解構函式前面加上關鍵字virtual,則該解構函式就稱為虛解構函式。
上述代碼擴充例子:
#include<iostream>using namespace std;class Base{ public: virtual ~Base(){} virtual void func() { cout<<"Base func"<<endl; } };class Derived1:public Base{ void func() { cout<<"Derived1 func"<<endl; } };class Derived2:public Derived1{ /* void func() { cout<<"Derived2 func"<<endl; }*/ };class Derived3:public Derived2{ void func() { cout<<"Derived3 func"<<endl; } };class Derived4:public Derived3{ void func() { cout<<"Derived4 func"<<endl; } };void test(Base &b)//引用方式 { b.func(); }int main(){ Base b; Derived1 d1; Derived2 d2; Derived3 d3; Derived4 d4; test(b); test(d1); test(d2); test(d3); test(d4); system("pause"); }
注意d2使用的是其父類的成員函數。
在使用虛解構函式時,要注意以下兩點:
1.只要基類的解構函式被聲明為虛函數,則衍生類別的解構函式,無論是否使用virtual關鍵字進行聲明,都自動成為虛函數。
2.如果基類的解構函式為虛函數,則當衍生類別未定義解構函式時,編譯器所產生的解構函式也為虛函數。
一般來說,在程式中最好把基類的解構函式聲明為虛函數。這將使所有衍生類別的解構函式自動成為虛函數。這樣,如果程式中用delete運算子準備刪除一個對象,而delete運算子操作對象是指向衍生類別的基類指標,系統會調用相應的解構函式;否則系統只執行蕨類的解構函式,而不執行衍生類別的解構函式,從而可能導致異常情況的發生。
建構函式不能聲明為虛函數。這是因為在執行建構函式時類對象還未完成建立過程,當然談不上函數與類對象的關聯。
補充例子:
#include <iostream> using namespace std; class A { protected: int m_data; public: A(int data = 0):m_data(data) { } int GetData() { return doGetData(); } virtual int doGetData() { return m_data; } }; class B:public A { protected: int m_data; public: B(int data = 1): m_data(data) { } int doGetData() { return m_data; } }; class C:public B { protected: int m_data; public: C(int data = 2):m_data(data) { } }; int main() { C c(10); cout << c.GetData() << endl; cout << c.A::GetData() << endl; cout << c.B::GetData() << endl; cout << c.C::GetData() << endl; cout << c.doGetData() << endl; cout << c.A::doGetData() << endl; cout << c.B::doGetData() << endl; cout << c.C::doGetData() << endl; system("pause"); return 0; }
1 //C中getdata未定義,B中也是,所以調用A的。A的dogetdata是虛函數,所以調用B的,返回B::m_data1 //同上1 //同上1 //同上1 //C中dogetdata未定義,調用B的,返回B::m_data0 //直接調用A中的dogetdata,返回A::m_data1 //調用B中doggetdata,返回B::m_data1 //同c.doGetData()Press any key to continue . . .
3.純虛函數與抽象類別
抽象類別是一種特殊的類,它提供統一的操作介面。建立抽象類別是為了多態地使用抽象類別成員函數。抽象類別是包含純虛函數的類。
3.1 純虛函數
定義形式:
virtual <函數類型><函數名>(參數表)=0;
純虛函數的作用是為衍生類別提供一個統一的介面,純虛函數的實現可以留給衍生類別來完成。一般來說,一個抽象類別中帶有至少一個純虛函數。
例:
#include<iostream>using namespace std;class Point{ protected: int x0,y0; public: Point(int i,int j) { x0=i; y0=j; } virtual void set()=0; virtual void draw()=0; };class Line:public Point{ protected: int x1,y1; public: Line(int i=0,int j=0,int m=0,int n=0):Point(i,j) { x1=m; y1=n; } void set(){cout<<"Line_set()"<<endl;} void draw(){cout<<"Line_draw()"<<endl;} };class Ellipse:public Point{ protected: int x2,y2; public: Ellipse(int i=0,int j=0,int p=0,int q=0):Point(i,j) { x2=p; y2=q; } void set(){cout<<"Ellipse_set()"<<endl;} void draw(){cout<<"Ellipse_draw()"<<endl;} };void drawobj(Point *p){ p->draw(); }void setobj(Point *p){ p->set(); }int main(){ Line *li=new Line();//new Line; drawobj(li); setobj(li); cout<<endl; Ellipse *el=new Ellipse();//new Ellipse; drawobj(el); setobj(el); cout<<endl; Line *li2=new Line; drawobj(li2); setobj(li2); cout<<endl; Ellipse elp; drawobj(&elp); setobj(&elp); cout<<endl; system("pause"); }
在上述代碼中,類Line和類Ellipse都公有繼承自類Point,在該程式中要實現多態,就必須定義set()函數和draw()函數為虛函數。同時,由於點是不需要在應用程式中畫出的,因此Point的set()函數和draw()函數定義為虛函數。在主函數main()中,建立Line對象和Ellipse對象後,調用這兩個同名的函數即可。這就是虛函數的作用,它一般用於基類函數沒有具體操作,而衍生類別中該函數可能需要的場合。
3.2 抽象類別
包含虛函數的類稱為抽象類別。抽象類別是一種特殊的類,是為了抽象和設計的目的而建立的,處於繼承階層的較上層。抽象類別是不能建立對象的,在實際中為了強調一個類是抽象類別,可將該類的建構函式聲明為保護的存取控制許可權。
抽象類別的主要作用是將有關的類組織在一個繼承層次中,由抽象類別來為它們提供一個公用的根,相關的子類是從這個根派生出來的。抽象類別刻畫了一組子類的操作介面的通用語義,這些語義也傳給子類。一般而言,抽象類別只描述這組子類共同的操作介面,而完整的實現留給子類。使用抽象類別時應該注意以下問題:
1.抽象類別只能用做其他類的基類,不能建立抽象類別對象,因為它的純虛函數沒有定義功能,其純屬虛函數的實現由衍生類別給出。
2.抽象類別不能用做參數類型、函數的傳回型別或顯式轉換的類型。
3.可以聲明抽象類別的對象指標或對象引用,從而可以訪問衍生類別對象成員,實現動態聯編。
4.若衍生類別中沒有給出抽象類別的所有純虛函數的函數體,衍生類別仍是一個抽象類別。若抽象類別的衍生類別中給出了所有純虛函數的函數體,這個衍生類別不再是一個抽象類別,它可以建立自己的對象了。例:
#include<iostream>using namespace std;class Vehicle{ protected: float speed; int total; public: Vehicle(float speed,int total) { Vehicle::speed = speed;//成員變數和函數參數重名,注意加上::以區分成員變數和函數參數,否則初始化會有問題 Vehicle::total = total;//或者this->total = total; } virtual void ShowMember()=0; };class Car:public Vehicle{ protected: int aird; public: Car(int aird,float speed,int total):Vehicle(speed,total) { Car::aird = aird; } void ShowMember() { cout<<"Speed is: "<<speed<<endl; cout<<"Toatal is: "<<total<<endl; cout<<"Aird is: "<<aird<<endl; } };int main(){ Car c(250,150,4); c.ShowMember(); system("pause"); }