[例12.1] 先建立一個Point(點)類,包含數據成員x,y(坐標點)。以它為基類,派生出一個Circle(圓)類,增加數據成員r(半徑),再以Circle類為直接基類,派生出一個Cylinder(圓柱體)類,再增加數據成員h(高)。要求編寫程序,重載運算符“<<”和“>>”,使之能用於輸出以上類對象。
這個例題難度不大,但程序很長。對於一個比較大的程序,應當分成若干步驟進行。先聲明基類,再聲明派生類,逐級進行,分步調試。
1) 聲明基類Point
類可寫出聲明基類Point的部分如下:
#include <iostream> //聲明類Point class Point { public: Point(float x=0,float y=0); //有默認參數的構造函數 void setPoint(float ,float); //設置坐標值 float getX( )const {return x;} //讀x坐標 float getY( )const {return y;} //讀y坐標 friend ostream & operator <<(ostream &,const Point &); //重載運算符“<<” protected: //受保護成員 float x, y; }; //下面定義Point類的成員函數 Point::Point(float a,float b) //Point的構造函數 { //對x,y初始化 x=a; y=b; } void Point::setPoint(float a,float b) //設置x和y的坐標值 { //為x,y賦新值 x=a; y=b; } //重載運算符“<<”,使之能輸出點的坐標 ostream & operator <<(ostream &output, const Point &p) { output<<"["<<p.x<<","<<p.y<<"]"<<endl; return output; }
以上完成了基類Point類的聲明。
為了提高程序調試的效率,提倡對程序分步調試,不要將一個長的程序都寫完以后才統一調試,那樣在編譯時可能會同時出現大量的編譯錯誤,面對一個長的程序,程序人員往往難以迅速准確地找到出錯位置。要善於將一個大的程序分解為若干個文件,分別編譯,或者分步調試,先通過最基本的部分,再逐步擴充。
現在要對上面寫的基類聲明進行調試,檢查它是否有錯,為此要寫出main函數。實際上它是一個測試程序。
int main( ) { Point p(3.5,6.4); //建立Point類對象p cout<<"x="<<p.getX( )<<",y="<<p.getY( )<<endl; //輸出p的坐標值 p.setPoint(8.5,6.8); //重新設置p的坐標值 cout<<"p(new):"<<p<<endl; //用重載運算符“<<”輸出p點坐標 return 0; }
getX和getY函數聲明為常成員函數,作用是只允許函數引用類中的數據,而不允許修改它們,以保證類中數據的安全。數據成員x和y聲明為protected,這樣可以被派生類訪問(如果聲明為private,派生類是不能訪問的)。
程序編譯通過,運行結果為:
x=3.5,y=6.4
p(new):[8.5,6.8]
測試程序檢查了基類中各函數的功能,以及運算符重載的作用,證明程序是正確的。
2)聲明派生類Circle
在上面的基礎上,再寫出聲明派生類Circle的部分:
class Circle:public Point //circle是Point類的公用派生類 { public: Circle(float x=0,float y=0,float r=0); //構造函數 void setRadius(float ); //設置半徑值 float getRadius( )const; //讀取半徑值 float area ( )const; //計算圓面積 friend ostream &operator <<(ostream &,const Circle &); //重載運算符“<<” private: float radius; }; //定義構造函數,對圓心坐標和半徑初始化 Circle::Circle(float a,float b,float r):Point(a,b),radius(r){} //設置半徑值 void Circle::setRadius(float r){radius=r;} //讀取半徑值 float Circle::getRadius( )const {return radius;} //計算圓面積 float Circle::area( )const { return 3.14159*radius*radius; } //重載運算符“<<”,使之按規定的形式輸出圓的信息 ostream &operator <<(ostream &output,const Circle &c) { output<<"Center=["<<c.x<<","<<c.y<<"],r="<<c.radius<<",area="<<c.area( )<<endl; return output; }
為了測試以上Circle類的定義,可以寫出下面的主函數:
int main( ) { Circle c(3.5,6.4,5.2); //建立Circle類對象c,並給定圓心坐標和半徑 cout<<"original circle:\\nx="<<c.getX()<<", y="<<c.getY()<<", r="<<c.getRadius( )<<", area="<<c.area( )<<endl; //輸出圓心坐標、半徑和面積 c.setRadius(7.5); //設置半徑值 c.setPoint(5,5); //設置圓心坐標值x,y cout<<"new circle:\\n"<<c; //用重載運算符“<<”輸出圓對象的信息 Point &pRef=c; //pRef是Point類的引用變量,被c初始化 cout<<"pRef:"<<pRef; //輸出pRef的信息 return 0; }
程序編譯通過,運行結果為:
original circle:(輸出原來的圓的數據)
x=3.5, y=6.4, r=5.2, area=84.9486
new circle:(輸出修改后的圓的數據)
Center=[5,5], r=7.5, area=176.714
pRef:[5,5] (輸出圓的圓心“點”的數據)
可以看到,在Point類中聲明了一次運算符“ <<”重載函數,在Circle類中又聲明了一次運算符“ <<”,兩次重載的運算符“<<”內容是不同的,在編譯時編譯系統會根據輸出項的類型確定調用哪一個運算符重載函數。main函數第7行用“cout<< ”輸出c,調用的是在Circle類中聲明的運算符重載函數。
請注意main函數第8行:
Point & pRef = c;
定義了 Point類的引用變量pRef,並用派生類Circle對象c對其初始化。前面我們已經講過,派生類對象可以替代基類對象為基類對象的引用初始化或賦值(詳情請查看:C++基類與派生類的轉換)。現在 Circle是Point的公用派生類,因此,pRef不能認為是c的別名,它得到了c的起始地址, 它只是c中基類部分的別名,與c中基類部分共享同一段存儲單元。所以用“cout<<pRef”輸出時,調用的不是在Circle中聲明的運算符重載函數,而是在Point中聲明的運算符重載函數,輸出的是“點”的信息,而不是“圓”的信息。
3) 聲明Circle的派生類Cylinder
前面已從基類Point派生出Circle類,現在再從Circle派生出Cylinder類。
class Cylinder:public Circle// Cylinder是Circle的公用派生類 { public: Cylinder (float x=0,float y=0,float r=0,float h=0); //構造函數 void setHeight(float ); //設置圓柱高 float getHeight( )const; //讀取圓柱高 loat area( )const; //計算圓表面積 float volume( )const; //計算圓柱體積 friend ostream& operator <<(ostream&,const Cylinder&); //重載運算符<< protected: float height;//圓柱高 }; //定義構造函數 Cylinder::Cylinder(float a,float b,float r,float h):Circle(a,b,r),height(h){} //設置圓柱高 void Cylinder::setHeight(float h){height=h;} //讀取圓柱高 float Cylinder::getHeight( )const {return height;} //計算圓表面積 float Cylinder::area( )const { return 2*Circle::area( )+2*3.14159*radius*height;} //計算圓柱體積 float Cylinder::volume()const {return Circle::area()*height;} ostream &operator <<(ostream &output,const Cylinder& cy) { output<<"Center=["<<cy.x<<","<<cy.y<<"],r="<<cy.radius<<",h="<<cy.height <<"\\narea="<<cy.area( )<<", volume="<<cy.volume( )<<endl; return output; } //重載運算符“<<”
可以寫出下面的主函數:
int main( ) { Cylinder cy1(3.5,6.4,5.2,10);//定義Cylinder類對象cy1 cout<<"\\noriginal cylinder:\\nx="<<cy1.getX( )<<", y="<<cy1.getY( )<<", r=" <<cy1.getRadius( )<<", h="<<cy1.getHeight( )<<"\\narea="<<cy1.area() <<",volume="<<cy1.volume()<<endl;//用系統定義的運算符“<<”輸出cy1的數據 cy1.setHeight(15);//設置圓柱高 cy1.setRadius(7.5);//設置圓半徑 cy1.setPoint(5,5);//設置圓心坐標值x,y cout<<"\\nnew cylinder:\\n"<<cy1;//用重載運算符“<<”輸出cy1的數據 Point &pRef=cy1;//pRef是Point類對象的引用變量 cout<<"\\npRef as a Point:"<<pRef;//pRef作為一個“點”輸出 Circle &cRef=cy1;//cRef是Circle類對象的引用變量 cout<<"\\ncRef as a Circle:"<<cRef;//cRef作為一個“圓”輸出 return 0; }
運行結果如下:
original cylinder:(輸出cy1的初始值)
x=3.5, y=6.4, r=5.2, h=10 (圓心坐標x,y。半徑r,高h)
area=496.623, volume=849.486 (圓柱表面積area和體積volume)
new cylinder: (輸出cy1的新值)
Center=[5,5], r=7.5, h=15 (以[5,5]形式輸出圓心坐標)
area=1060.29, volume=2650.72(圓柱表面積area和體積volume)
pRef as a Point:[5,5] (pRef作為一個“點”輸出)
cRef as a Circle:Center=[5,5], r=7.5, area=176.714(cRef作為一個“圓”輸出)
說明:在Cylinder類中定義了 area函數,它與Circle類中的area函數同名,根據前面我們講解的同名覆蓋的原則(詳情請查看:C++多重繼承的二義性問題),cy1.area( ) 調用的是Cylinder類的area函數(求圓柱表面積),而不是Circle類的area函數(圓面積)。請注意,這兩個area函數不是重載函數,它們不僅函數名相同,而且函數類型和參數個數都相同,兩個同名函數不在同 —個類中,而是分別在基類和派生類中,屬於同名覆蓋。重載函數的參數個數和參數類型必須至少有一者不同,否則系統無法確定調用哪一個函數。
main函數第9行用“cout<<cy1”來輸出cy1,此時調用的是在Cylinder類中聲明的重載運算符“<<”,按在重載時規定的方式輸出圓柱體cy1的有關數據。
main函數中最后4行的含義與在定義Circle類時的情況類似。pRef是Point類的引用變量,用cy1對其初始化,但它不是cy1的別名,只是cy1中基類Point部分的別名,在輸出pRef時是作為一個Point類對象輸出的,也就是說,它是一個“點”。同樣,cRef是Circle類的引用變量,用cy1對其初始化,但它只是cy1中的直接基類Circle部分的別名, 在輸出 cRef 時是作為Circle類對象輸出的,它是一個"圓”,而不是一個“圓柱體”。從輸 出的結果可以看出調用的是哪個運算符函數。