C面向對象程序設計第5版_第1頁
C面向對象程序設計第5版_第2頁
C面向對象程序設計第5版_第3頁
C面向對象程序設計第5版_第4頁
C面向對象程序設計第5版_第5頁
已閱讀5頁,還剩49頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第5章多態(tài)性與虛函數5.1多態(tài)性的概念5.2一個典型的例子5.3虛函數5.4純虛函數與抽象類5.1多態(tài)性的概念在C++中,基類指針或引用可以直接引用其任何派生子類,而無需程序員介入——這種“用基類的指針或引用操縱多個類型”的能力稱為多態(tài)(polymorphism)。多態(tài)性(polymorphism)是面向對象程序設計的一個重要特征。利用多態(tài)性可以設計和實現(xiàn)一個易于擴展的系統(tǒng)。例如,已知基類Camera派生出兩個子類OrthographicCamera和PerspectiveCamera:已知函數:

voidlookAt(constCamera*pcamera);CameraPerspectiveCameraOrthographicCamera每次lookAt()函數調用時都會傳入一個Camera子類對象的地址,編譯器會自動地把它轉換成適當的基類指針。例如:

OrthographicCamera

ocam;

lookAt(&ocam);//ok:自動轉換成Camera*

PerspectiveCamera*pcam=newPerspectiveCamera;

lookAt(pcam);//ok:自動被轉換成Camera*lookAt()的實現(xiàn)被屏蔽在應用程序的實際Camera子類之外,如果以后增加或刪除一個子類,無需改變lookAt().子類多態(tài)性使得我們在編寫應用程序的核心時,可以不用考慮將來需要維護的單個類型。從系統(tǒng)實現(xiàn)的角度看,多態(tài)性分為兩類:靜態(tài)多態(tài)性和動態(tài)多態(tài)性。靜態(tài)多態(tài)性是通過函數的重載實現(xiàn)的。利用基類指針和引用,對抽象基類的公有接口進行編程。在運行時刻,真正要引用的類型被解析出來,并且調用適當的公有接口實例。多態(tài)性是“一個接口,多種方法”。在運行時刻解析出被調用的函數,這個解析過程被稱為動態(tài)綁定(dynamicbinding)。在C++中,通過虛擬函數機制來支持動態(tài)綁定。通過繼承和動態(tài)綁定,子類型多態(tài)性為面向對象的程序設計提供了基礎。5.2一個典型的例子下面是一個承上啟下的例子。一方面它是有關繼承和運算符重載內容的綜合應用的例子,通過這個例子可以進一步融會貫通前面所學的內容,另一方面又是作為討論多態(tài)性的一個基礎用例。例5.1:

先建立一個Point(點)類,包含數據成員x,y(坐標點)。以它為基類,派生出一個Circle(圓)類,增加數據成員r(半徑),再以Circle類為直接基類,派生出一個Cylinder(圓柱體)類,再增加數據成員h(高)。要求編寫程序,重載運算符“<<”和“>>”,使之能用于輸出以上類對象。聲明基類Point類可寫出聲明基類Point的部分如下:#include<iostream>//聲明類PointclassPoint{public:

Point(floatx=0,floaty=0);//有默認參數的構造函數 voidsetPoint(float,float);//設置坐標值

floatgetX()const{returnx;}//讀x坐標

floatgetY()const{returny;}//讀y坐標

friendostream&operator<<(ostream&,constPoint&);//重載運算符“<<”protected://受保護成員

floatx,y;};//下面定義Point類的成員函數//Point的構造函數Point::Point(floata,floatb)//對x,y初始化{x=a;y=b;}//設置x和y的坐標值voidPoint::setPoint(floata,floatb)//為x,y賦新值{x=a;y=b;}//重載運算符“<<”,使之能輸出點的坐標ostream&operator<<(ostream&output,constPoint&p){output<<″[″<<p.x<<″,″<<p.y<<″]″<<endl;returnoutput;}編寫測試程序main函數:intmain(){Pointp(3.5,5.4);//建立Point類對象p

cout<<″x=″<<p.getX()<<″,y=″<<p.getY()<<endl;//輸出p的坐標值

p.setPoint(8.5,5.8);//重新設置p的坐標值

cout<<″p(new):″<<p<<endl;//用重載運算符“<<”輸出p點坐標}程序編譯通過,運行結果為x=3.5,y=5.4p(new):[8.5,5.8]測試程序檢查了基類中各函數的功能,以及運算符重載的作用,證明程序是正確的。(2)聲明派生類CircleclassCircle:publicPoint//circle是Point類的公用派生類{public:

Circle(floatx=0,floaty=0,floatr=0);//構造函數

voidsetRadius(float);//設置半徑值

floatgetRadius()const;//讀取半徑值

floatarea()const;//計算圓面積

friendostream&operator<<(ostream&,constCircle&);//重載運算符“<<”

private: floatradius;};//定義構造函數,對圓心坐標和半徑初始化Circle::Circle(float

a,float

b,float

r):Point(a,b),radius(r){}//設置半徑值voidCircle::setRadius(floatr){radius=r;}//讀取半徑值floatCircle::getRadius()const{returnradius;}//計算圓面積floatCircle::area()const{return3.14159*radius*radius;}//重載運算符“<<”,使之按規(guī)定的形式輸出圓的信息ostream&operator<<(ostream&output,constCircle&c){output<<″Center=[″<<c.x<<″,″<<c.y<<″],r=″<<c.radius<<″,area=″<<c.area()<<endl;returnoutput;}測試Circle類定義的主函數:intmain(){Circlec(3.5,5.4,5.2);//建立Circle類對象c,并給定圓心坐標和半徑

cout<<″originalcircle:\\nx=″<<c.getX()<<″,y=″<<c.getY()<<″,r=″<<c.getRadius()<<″,area=″<<c.area()<<endl;//輸出圓心坐標、半徑和面積

c.setRadius(7.5);//設置半徑值

c.setPoint(5,5);//設置圓心坐標值x,y

cout<<″newcircle:\\n″<<c;//用重載運算符“<<”輸出圓對象的信息

Point&pRef=c;//pRef是Point類的引用變量,被c初始化

cout<<″pRef:″<<pRef;//輸出pRef的信息

return0;}程序編譯通過,運行結果為:originalcircle:x=3.5,y=5.4,r=5.2,area=84.9485newcircle:Center=[5,5],r=7.5,area=175.714pRef:[5,5](3)聲明Circle的派生類CylinderclassCylinder:publicCircle//Cylinder是Circle的公用派生類{public: Cylinder(floatx=0,floaty=0,floatr=0,floath=0);

voidsetHeight(float);//設置圓柱高

floatgetHeight()const;//讀取圓柱高

floatarea()const;//計算圓表面積

floatvolume()const;//計算圓柱體積

friendostream&operator<<(ostream&,constCylinder&);

protected: floatheight;//圓柱高};//定義構造函數Cylinder::Cylinder(float

a,float

b,float

r,floath):Circle(a,b,r),height(h){}//設置圓柱高voidCylinder::setHeight(float

h){height=h;}//讀取圓柱高floatCylinder::getHeight()const{returnheight;}//計算圓表面積floatCylinder::area()const{return2*Circle::area()+2*3.14159*radius*height;}//計算圓柱體積floatCylinder::volume()const{returnCircle::area()*height;}//重載運算符“<<”ostream&operator<<(ostream&output,constCylinder&cy){output<<″Center=[″<<cy.x<<″,″<<cy.y<<″],r=″<<cy.radius<<″,h=″<<cy.height<<″\\narea=″<<cy.area()<<″,volume=″<<cy.volume()<<endl;returnoutput;}寫出下面的測試主函數:intmain(){ Cylindercy1(3.5,5.4,5.2,10);//定義Cylinder類對象cy1

cout<<″\\noriginalcylinder:\\nx=″<<cy1.getX() <<″,y=″<<cy1.getY()<<″,r=″ <<cy1.getRadius()<<″,h=″<<cy1.getHeight()<<″\\narea=″<<cy1.area() <<″,volume=″<<cy1.volume()<<endl;//用系統(tǒng)定義的運算符“<<”輸出cy1的數據

cy1.setHeight(15);//設置圓柱高

cy1.setRadius(7.5);//設置圓半徑

cy1.setPoint(5,5);//設置圓心坐標值x,y

cout<<″\\nnewcylinder:\\n″<<cy1;//用重載運算符“<<”輸出cy1的數據

Point&pRef=cy1;//pRef是Point類對象的引用變量

cout<<″\\npRefasaPoint:″<<pRef;//pRef作為一個“點”輸出

Circle&cRef=cy1;//cRef是Circle類對象的引用變量

cout<<″\\ncRefasaCircle:″<<cRef;//cRef作為一個“圓”輸出

return0;}運行結果如下:originalcylinder:(輸出cy1的初始值)x=3.5,y=5.4,r=5.2,h=10(圓心坐標x,y。半徑r,高h)area=495.523,volume=849.485(圓柱表面積area和體積volume)newcylinder:(輸出cy1的新值)Center=[5,5],r=7.5,h=15(以[5,5]形式輸出圓心坐標)area=1050.29,volume=2550.72(圓柱表面積area和體積volume)pRefasaPoint:[5,5](pRef作為一個“點”輸出)cRefasaCircle:Center=[5,5],r=7.5,area=175.714(cRef作為一個“圓”輸出)在本例中存在靜態(tài)多態(tài)性,這是運算符重載引起的??梢钥吹?,在編譯時編譯系統(tǒng)即可以判定應調用哪個重載運算符函數。5.3虛函數

5.3.1虛函數的作用在類的繼承層次結構中,在不同的層次中可以出現(xiàn)名字相同、參數個數和類型都相同而功能不同的函數。編譯系統(tǒng)按照同名覆蓋的原則決定調用的對象。例如:例5.1程序中cy1.area()調用的是派生類Cylinder中的成員函數area。如果想調用cy1中的直接基類Circle的area函數,改如何調用呢?應當表示為:cy1.Circle::area()。用這種方法來區(qū)分兩個同名的函數很不方便。能否用同一個調用形式,既能調用派生類又能調用基類的同名函數?可以在程序中通過指針調用基類和派生類的同名函數,根據指針實際指向的對象來調用對應的函數。例如,語句pt->display();

可以調用不同派生層次中的display函數,只需在調用前給指針變量pt賦以不同的值(使之指向不同的類對象)即可。C++通過虛函數來解決這個問題。虛函數的作用是允許在派生類中重新定義與基類同名的函數,并且可以通過基類指針或引用來訪問基類和派生類中的同名函數。我們來分析例5.2。這個例子開始時沒有使用虛函數,然后再討論使用虛函數的情況。例5.2基類Student與派生類Graduate有同名函數。#include<iostream>#include<string>usingnamespacestd;//聲明基類StudentclassStudent{ public:

Student(int,string,float);//聲明構造函數

voiddisplay();/聲明輸出函數

protected://受保護成員,派生類可以訪問

intnum; stringname; floatscore;};//Student類成員函數的實現(xiàn)Student::Student(intn,stringnam,floats)//定義構造函數{num=n;name=nam;score=s;}voidStudent::display()//定義輸出函數{cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<score<<″\\n\\n″;}//聲明公用派生類GraduateclassGraduate:publicStudent{public:

Graduate(int,string,float,float);//聲明構造函數

voiddisplay();//聲明輸出函數private:floatpay;};//Graduate類成員函數的實現(xiàn)Graduate::Graduate(intn,stringnam,float

s,float

p):Student(n,nam,s),pay(p){}voidGraduate::display()//定義輸出函數{cout<<″num:″<<num<<″\\nname:″<<name<<″\\nscore:″<<score<<″\\npay=″<<pay<<endl;}//主函數intmain(){ Studentstud1(1001,“Li”,87.5); Graduategrad1(2001,“Wang”,98.5,563.5); Student*pt=&stud1; pt->display(); pt=&grad1; pt->display(); return0;}運行結果如下,請仔細分析。 num:1001

name:Li score:87.5 num:2001

name:wang score:98.5對程序作一點修改,在Student類中聲明display函數時,在最左面加一個關鍵字virtual,即

virtualvoiddisplay();再編譯和運行程序,請注意分析運行結果:num:1001name:Liscore:87.5num:2001name:wangscore:98.5pay=12005.3.2C++中的虛函數假定我們要為汽車零件商店設計一個記帳程序。希望該程序非常靈活,能夠兼容以后出現(xiàn)的新銷售情況。為了適應這種情況,將計算賬單的函數設為虛函數。設計基類Sale,Sale類對應于單件商品的簡單銷售。所有類型的銷售(如打折銷售)都是Sale類的派生類。由虛函數實現(xiàn)的動態(tài)多態(tài)性就是:基類的指針或引用可以指向其任意派生類的能力。虛函數的使用方法是:在基類用virtual聲明成員函數為虛函數。在派生類中重新定義此函數,要求函數名、函數類型、函數參數個數和類型全部與基類的虛函數相同,并根據派生類的需要重新定義函數體。定義一個指向基類對象的指針變量,并使它指向同一類繼承層次結構中的某個對象。通過該指針變量調用此虛函數,此時調用的就是指針變量指向的對象的同名函數。C++規(guī)定,當一個成員函數被聲明為虛函數后,其派生類中的同名函數都自動成為虛函數。在派生類重新聲明該虛函數時,可以加virtual,也可以不加,但習慣上一般在每一層聲明該函數時都加virtual,使程序更加清晰。如果在派生類中沒有對基類的虛函數重新定義,則派生類簡單地繼承其直接基類的虛函數。特別提醒:有時在基類中定義的非虛函數會在派生類中被重新定義(如例5.1中的area函數),此時不存在多態(tài)性。5.3.3靜態(tài)關聯(lián)與動態(tài)關聯(lián)計算機系統(tǒng)如何選擇正確的調用對象,從而實現(xiàn)多態(tài)性?確定調用的具體對象的過程稱為關聯(lián)(binding)。在這里是指把一個函數名與一個類對象捆綁在一起,建立關聯(lián)。一般地說,關聯(lián)指把一個標識符和一個存儲地址聯(lián)系起來。靜態(tài)關聯(lián)(staticbinding)和動態(tài)關聯(lián)(dynamicbinding)(晚期綁定)在編譯時即可確定其調用的函數屬于哪一個類,其過程稱為靜態(tài)關聯(lián)(staticbinding),又稱為早期關聯(lián)(earlybinding)。函數重載屬靜態(tài)關聯(lián)。對于通過基類指針調用虛函數,編譯系統(tǒng)在編譯該行時是無法確定調用哪一個類對象的虛函數的。在這樣的情況下,在運行階段確定關聯(lián)關系。此過程稱為動態(tài)關聯(lián)(dynamicbinding)。這種多態(tài)性是運行階段的多態(tài)性。在運行階段,指針可以先后指向不同的類對象,從而調用同一類族中不同類的虛函數。由于動態(tài)關聯(lián)是在編譯以后的運行階段進行的,因此也稱為晚期關聯(lián)(latebinding)。5.3.4在什么情況下應當聲明虛函數使用虛函數時,有兩點要注意:(1)虛函數只能作用于類的繼承層次結構中,即多態(tài)性只存在于類繼承層次中。(2)一個成員函數被聲明為虛函數后,在同一類族中的類就不能再定義一個非virtual的但與該虛函數具有相同的參數(包括個數和類型)和函數返回值類型的同名函數。根據什么考慮是否把一個成員函數聲明為虛函數呢?主要考慮以下幾點:(1)首先看成員函數所在的類是否會作為基類。然后看成員函數在類的繼承后有無可能被更改功能,如果希望更改其功能的,一般應該將它聲明為虛函數。(2)如果成員函數在類被繼承后功能不需修改,或派生類用不到該函數,則不要把它聲明為虛函數。(3)應考慮對成員函數的調用是通過對象名還是通過基類指針或引用去訪問,如果是通過基類指針或引用去訪問的,則應當聲明為虛函數。(4)有時會在基類定義具有空函數體的虛函數。它的作用只是定義了一個虛函數名(即公有接口),然后通過派生類來添加具體功能。在5.4節(jié)中將詳細討論此問題。(5)編譯器和運行環(huán)境需要為虛函數做較多的工作,所以,如果將不必要設置為虛函數的函數標記為virtual,會影響程序的執(zhí)行效率。5.3.5虛析構函數繼承機制下的析構函數的行為如下:派生類的析構函數先被調用。完成之后,直接基類的析構函數被靜態(tài)調用。但是,如果用new運算符建立了臨時對象,若基類中有析構函數,并且定義了一個指向該基類的指針變量。在程序用帶指針參數的delete運算符撤銷對象時,會發(fā)生一個情況:系統(tǒng)會只執(zhí)行基類的析構函數,而不執(zhí)行派生類的析構函數。例5.3基類中有非虛析構函數時的執(zhí)行情況。(為簡化程序,只列出最必要的部分。)#include<iostream>usingnamespacestd;classPoint//定義基類Point類{public:Point(){}//Point類構造函數~Point(){cout<<″executingPointdestructor″<<endl;}//Point類析構函數};classCircle:publicPoint//定義派生類Circle類{public:Circle(){}//Circle類構造函數~Circle(){cout<<″executingCircledestructor″<<endl;}//Circle類析構函數

private:

intradius;};intmain(){Point*p=newCircle;//用new開辟動態(tài)存儲空間deletep;//用delete釋放動態(tài)存儲空間return0;}運行結果為:executingPointdestructor表示只執(zhí)行了基類Point的析構函數,而沒有執(zhí)行派生類Circle的析構函數。為了執(zhí)行派生類Circle的析構函數,需要將基類的析構函數聲明為虛析構函數,即

virtual~Point(){cout<<″executingPointdestructor″<<endl;}程序其他部分不改動,再運行程序,結果為 executingCircledestructor executingPointdestructor如果將基類的析構函數聲明為虛函數時,由該基類所派生的所有派生類的析構函數也都自動成為虛函數,即使派生類的析構函數與基類的析構函數名字不相同。最好把基類的析構函數聲明為虛函數。這將使所有派生類的析構函數自動成為虛函數。構造函數不能聲明為虛函數。這是因為在執(zhí)行構造函數時類對象還未完成建立過程,當然談不上函數與類對象的綁定。5.4純虛函數與抽象類

5.4.1純虛函數C++語言提供了一種語法結構,通過它表明,一個虛擬函數只是提供了一個可被派生子類型改寫的接口,它本身并不能通過虛擬機制被調用——純虛擬函數例如在例5.1程序中,基類Point可以提供求面積的純虛擬函數:

virtualfloatarea()const=0;//純虛函數其直接派生類Circle和間接派生類Cylinder可以改寫這個area函數,分別為求圓面積和求圓柱體表面積。純虛函數是在聲明虛函數時被“初始化”為0的函數。聲明純虛函數的一般形式是 virtual函數類型函數名(參數表列)=0; 注意: ①純虛函數沒有函數體; ②最后面的“=0”并不表示函數返回值為0,它只起形式上的作用,告訴編譯系統(tǒng)“這是純虛函數”; ③這是一個聲明語句,最后應有分號。5.4.2抽象類包含一個或多個純虛函數的類稱為抽象基類(abstractclass)。試圖創(chuàng)建一個抽象基類的獨立類對象會導致編譯錯誤。抽象類的作用是作為一個類繼承層次結構的共同基類,或者說,為一個類繼承層次結構提供一個公共接口。這種類不用來定義對象,一般只作為一種被繼承的基本類型。如果在派生類中對基類的所有純虛函數進行了定義,那么這些函數就被賦予了功能,可以被調用。這時派生類是可以用來定義對象的具體類(concreteclass);否則派生類仍然是抽象類,不能用來定義對象。我們可以定義指向抽象類對象的指針變量,用此指針指向具體派生類的對象,然后通過該指針調用虛函數,實現(xiàn)多態(tài)性的操作。5.4.3應用實例例5.4虛函數和抽象基類的應用。在例5.1介紹了以Point為基類的點—圓—圓柱體類的層次結構?,F(xiàn)在對它進行改寫,在程序中使用虛函數和抽象基類。類的層次結構的頂層是抽象基類Shape(形狀)。Point(點),Circle(圓),Cylinder(圓柱體)都是Shape類的直接派生類和間接派生類。程序如下:第(1)部分#include<iostream>usingnamespacestd;//聲明抽象基類ShapeclassShape{ public:

virtualfloatarea()const{return0.0;}

virtualfloatvolume()const{return0.0;}

virtualvoidshapeName()const=0;

};

第(2)部分//聲明Point類classPoint:publicShape//Point是Shape的公用派生類{public:

Point(float=0,float=0); voidsetPoint(float,float); floatgetX()const{returnx;} floatgetY()const{returny;} virtualvoidshapeName()const{cout<<″Point:″;} //對虛函數進行再定義

friendostream&operator<<(ostream&,constPoint&); protected: floatx,y;};//定義Point類成員函數Point::Point(float

a,floatb){x=a;y=b;}voidPoint::setPoint(float

a,floatb){x=a;y=b;}ostream&operator<<(ostream&output,constPoint&p){ output<<″[″<<p.x<<″,″<<p.y<<″]″; returnoutput;}第(3)部分//聲明Circle類classCircle:publicPoint{ public:

Circle(floatx=0,floaty=0,floatr=0); voidsetRadius(float); floatgetRadius()const; virtualfloatarea()const; virtualvoidshapeName()const {cout<<″Circle:″;}//對虛函數進行再定義

friendostream&operator<<(ostream&,constCircle&); protected: floatradius;};//聲明Circle類成員函數Circle::Circle(float

a,float

b,float

r):Point(a,b),radius(r){}voidCircle::setRadius(float

r):radius(r){}floatCircle::getRadius()const{returnradius;}floatCircle::area()const{return3.14159*radius*radius;}ostream&operator<<(ostream&output,constCircle&c){ output<<″[″<<c.x<<″,″<<c.y<<″],r=″<<c.radius; returnoutput;}第(4)部分//聲明Cylinder類classCylinder:publicCircle{ public: Cylinder(floatx=0,floaty=0,floatr=0,floath=0); voidsetHeight(float); virtualfloatarea()const; virtualfloatvolume()const; virtualvoidshapeName()const {cout<<″Cylinder:″;}//對虛函數進行再定義

friendostream&operator<<(ostream&,constCylinder&);protected: floatheight;};//定義Cylinder類成員函數Cylinder::Cylinder(float

a,float

b,float

r,floath):Circle(a,b,r),height(h){}voidCylinder::setHeight(float

h){height=h;}floatCylinder::area()const{return2*Circle::area()+2*3.14159*radius*height;}floatCylinder::volume()const{returnCircle::area()*height;}ostream&operator<<(ostream&output,constCylinder&cy){ output<<″[″<<cy.x<<″,″<<cy.y<<″],r=″<<cy.radius<<″,h=″<<cy.height; returnoutput;}第(5)部分//main函數intmain(){Pointpoint(3.2,4.5);//建立Point類對象pointCirclecircle(2.4,1.2,5.5);//建立Circle類對象circleCylindercylinder(3.5,5.4,5.2,10.5);//建立Cylinder類對象cylinder

point.shapeName();//靜態(tài)關聯(lián)

cout<<point<<endl;

circle.shapeName();//靜態(tài)關聯(lián)

cout<<circle<<endl;

cylinder.shapeName();//靜態(tài)關聯(lián)

cout<<cylinder<<endl<<endl;Shape*pt;//定義基類指針

pt=&point;//指針指向Point類對象

pt->shapeName();

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論