C++程式設計案例教程——Cha7多型 總結
面向物件技術三大特性:繼承、類的封裝、多型。
- 7.1多型的描述 7.1.1多型的含義 多型是指具有繼承關係的類,擁有相同的介面(函式名、形參和返回值),並允許有各自不同的實現,且一個物件例項只有在呼叫共同介面時,才能確定呼叫是何種實現,即“一個介面,多種實現”。 多型是指不同物件接受相同訊息後產生的不同動作。函式過載是一種多型,相同的函式名,對應多個不同函式體。在呼叫函式時,根據引數的不同執行了不同的函式體,產生了不同的動作。同理,運算子過載也是多型的典型應用,相同的運算子,可以用作不同型別資料進行運算,實質時呼叫了不同運算子過載函式 7.1.2多型的分類 面對象的多型可分為4類:過載多型、強制多型、包含多型和型別引數化多型。前兩者統稱為特殊性多型,用來刻化語義上無關聯的型別間的關係。後兩者稱為一般性多型,用來系統地刻畫語義上相關的一種型別。 函式過載和運算子過載都屬於過載多型。強制多型是指強制型別轉換。在派生類中宣告與基類同名的成員函式(引數必須相同,否則會被視為函式過載),並在基類中把該函式定義為虛擬函式,實現“一個介面,多種方式”,這就是包含多型。多型函式(或類)必須帶有型別,在使用時賦予實際的型別加以例項化,該型別引數確定函式(或類)在每次執行時運算元的型別,由於類模板例項化的各個類都具有相同的操作,而操作物件的型別卻各不相同,此種被稱為型別引數化多型。 7.1.3多型實現的方式 從實現角度分為編譯時多型和執行時多型。前者是在程式執行過程中才能動態地確定操作的具體物件,後者是在編譯的過程中確定了同名操作的具體操作物件。這種確定操作物件的過程稱為聯編或繫結。 靜態聯編對應於編譯時多型,動態聯編對應於執行時多型。靜態聯編是指在程式執行前將函式實現和函式呼叫關聯起來,也叫早繫結或前繫結。動態聯編是指在函式執行時才將函式實現和函式呼叫相關聯,也叫執行時綁、晚繫結或後繫結。 動態聯編靈活但速度相對較慢,而靜態聯編與之相反。
- 7.2運算子過載 運算子過載提供了一種機制,可以重新定義運算子,使之可以應用於使用者自定義型別。 7.2.1 運算子過載的定義 運算子過載是通過定義運算子過載函式來實現的。運算子過載函式的定義和一個普通函式定義類似。運算子過載函式採用兩種形式:成員函式和友元函式 。 1.過載為友元函式,函式定義格式如下: <型別> operator <運算子>(<引數列表>) { <函式體> } 然後在相應類中將該函式宣告為友元,宣告形式為: friend <型別> operator <運算子>(<引數列表>); 2.過載為成員函式,函式定義格式如下: <型別> <類名>:: operator <運算子>(<引數列表>) { <引數體> } 在以上兩種過載格式中,<引數列表>中引數的個數不僅取決於運算子的運算元個數,還取決於過載的形式,即過載為成員函式還是友元函式。 當過載為成員函式,引數個數比運算子的操作個數少一個,因為呼叫該成員函式本身也作為一個運算元參與運算,因此雙目運算子的引數個數是一個,單目運算子沒有引數。當過載為友元函式,引數個數與運算子的運算元個數相同。字尾++和字尾–為了與字首分開,會增加一個int型引數。 注: (1) 只能過載C++預定義的運算子,不能創造新的運算子。但不能過載 “ . ” 、“.* ” 、“ ::” 、“?:” 和“sizeof” 。 (2)過載後的運算子不能改變原有的優先順序、結合性以及運算元的個數。 (3)運算子過載函式的引數中至少有一個是自定義的型別。只用於簡單型別的運算子不能過載;也不能為簡單型別定義新的運算子。 (4)大多數運算子既可以過載為友元函式也可以過載為成員函式,一般將雙目運算子過載為友元函式,而將單目運算子過載為成員函式。 (5)對運算子過載時,儘量不要改變其內建的原有定義,如果改變了原來的含義,不如另起一個函式名字。 7.2.2 雙目運算子過載 雙目運算子即可以過載為類的成員函式,也可以過載為類的友元函式。兩者的區別是,對於同一個運算子,用成員函式比友元函式實現少一個引數。 1.雙目運算子過載為成員函式 例 7.1 “==”和“!=” 過載為成員函式。
#include <iostream> using namespace std; class Point { public: Point () {X=0; Y=0;} Point (double x, double y){X=x;Y=y; } bool operator==(const Point &); bool operator!=(const Point &); private: double X,Y; }; bool Point ::operator ==(const Point &p) //定義雙目關係運算符" =="過載運算子 { return((X==p.X)&&(Y==p.Y)); } bool Point :: opertaor !=(const Point &p ) //定義雙目關係運算符" != "過載函式 { return !((*this)==p); //呼叫運算子"=="過載函式 } int main () { Point p1,p2(2,3.4); if(p1==p2) cout<<"點p1和點p2相同"<<endl; if(p1!=p2) cout <<"點p1與點p2不相同"<<endl; } 說明:(1)需要判斷兩個類物件是否相等,過載關係運算符"==",比另外定義一個函式更符合人們的習慣。且一個運算子可以呼叫另一個已過載的運算子。 (2)程式中宣告一個點類Point,並對運算子"=="進行了過載定義,用來判斷兩個物件是否相等。如果兩個的橫縱標相等,表示兩個點相同,對運算子"!="進行過載函式時呼叫了運算子"=="的過載函式。
例 7.2算術運算子" + “、”-" 過載為成員函式。
#include<iostream>
using namespace std;
class Point
{
public:
Point (){ X=0;Y=0;}
Point (double x,double y){ X=x;Y=y;}
Point operator + (const Point & ); //算術運算子"+"過載為成員函式
Point operator - (const Point &); //算術運算子"-"過載為成員函式
void Dispoint()
{ cout <<"點的位置是:("<<X<<","<<Y<<")"<<endl;}
private:
double X,Y;
};
Point Point ::operator + (const Point &p) //定義算術運算子 " + " 過載函式
{
Point t (X+p.X,Y+p.Y);
return t;
}
Point Point ::operator - (const Point &p) //定義運算子"-"過載函式
{
Point t (X-p.X,Y-p.Y);
return t;
}
int main()
{
Point p1,p2(2,3.4),p3(1.2,6);
p1. Dispoint;
p1=p2+p3;
p1. Dispoint;
p1=p2-p3;
p1. Dispoint;
return 0;
}
說明:(1)兩個點類進行+-運算時,不能直接使用+ -,因此要過載+ -。
(2)由於計算時不能改變實參的值,所以形參型別採用常引用。
(3)雙目運算子過載為成員函式時,只有一個引數,另一個引數是this指標指向的的物件本身,this指標可以省略。
2.雙目運算子過載為友元函式 例7.3 將算術運算子和關係運算符過載為友元函式
#include<iostream>
using namespace std;
class Point
{
public:
Point (){ X=0;Y=0;}
Point (double x,double y){ X=x;Y=y;}
friend Point operator + (const Point &,const Point & ); //算術運算子"+"過載為成員函式
friend Point operator - (const Point &,const Point &); //算術運算子"-"過載為成員函式
friend bool operator==(const Point &,const Point &);
friend bool operator!=(const Point &,const Point &);
void Dispoint() //輸出點座標
{ cout <<"點的位置是:("<<X<<","<<Y<<")"<<endl;}
private:
double X,Y;
};
Point operator + (const Point &p1,const Point &p2) //定義運算子“+”過載函式
{
Point t(p1.X+p2.X,p1.Y+p1.Y);
return t;
}
Point operator - (const Point &p1,const Point &p2) //定義運算子“-”過載函式
{
Point t(p1.X-p2.X,p1.Y-p1.Y);
return t;
}
bool operator==(const Point &p1,const Point &p2) //定義關係運算符“==”過載函式
{
return ((p1.X==p2.X)&&(p1.Y==p2.Y));
}
bool operator!=(const Point &p1,const Point &p2) //定義關係運算符“!=”過載函式
{
return !(p1==p2);
}
main()
{
Point p1,p2(2,3.4),p3(1.2,6);
if(p2==p3) cout<<"p2=p3"<<endl;
if(p2!=p3) cout<<"p2!=p3"<<endl;
cout<<"p1的初始位置";
p1.Dispoint() ;
p1=p2+p3;
cout<<"p1的第一次移動位置";
p1.Dispoint() ;
p1=p2-p3;
cout<<"p1的第二次移動位置";
p1.Dispoint() ;
return 0;
}
說明:(1)雙目運算子過載為友元函式,運算子兩個運算元的引數都要出現在引數列表中。
(2)雙目運算子過載為友元函式,可以進行該類物件和其他型別資料的混合運算。
例:增加友元函式
Data operator + ( const int &);
Data operator +(const double &);
5+p1; 3.4+p2
加法第一個運算元是整型或浮點型,是內建簡單型別,簡單型別的運算子不能被重新定義,必須用友元函式來實現過載。
3.賦值運算子過載 如果對賦值運算子進行過載,它必須是類的成員函式。如果沒有對賦值運算進行過載,則會發生淺賦值,系統提供一個預設的賦值運算子過載函式,將右值物件的各個資料成員一一複製給左值物件的相應成員。為了防止記憶體洩漏,凡是定義建構函式的類都要對賦值運算子進行過載
例7.4賦值運算子過載
#include<iostream>
using namespace std;
class Point
{
public:
Point (){ X=0;Y=0;}
Point (double x,double y){ X=x;Y=y;}
Point& operator = (const Point &); //賦值運算子過載函式
Point& operator = (const int &); //賦值運算子過載函式
Point& operator = (const double &); //賦值運算子過載函式
void Dispoint() //輸出點座標
{ cout <<"點的位置是:("<<X<<","<<Y<<")"<<endl;}
private:
double X,Y;
};
Point& Point::operator = (const Point &d); //右值是Point型別的的賦值運算定義
{
if(this==&d) return *this ;//如果本物件給自己賦值,直接返回本物件
X=d.X;
Y=d.Y;
return *this;
}
Point& Point::operator = (const int &i); //右值是整型資料
{
X=i;
Y=0;
return *this;
}
Point& Point::operator = (const double &f); //右值是浮點型資料
{
X=f;
Y=0;
return *this;
}
int main()
{
Point p1,p2(2,3.4);
p1=p2;
cout<<"p1的位置";
p1.Dispoint();
p1=5;
cout<<"p1的位置";
p1.Dispoint();
p1=3.6;
cout<<"p1的位置";
p1.Dispoint();
return 0;
}
說明:(1)Point類定義了多個賦值運算過載符,滿足了在同類物件之間,也可以整型資料和浮點資料對Point類物件進行賦值。根據賦值運算子右運算元型別執行不同的函式。
(2)無論引數為何種型別,賦值運算子都必須過載為成員,並且因為返回的是左值,所以返回值得型別必須是該類的引用。
4.複合賦值運算子過載 複合賦值運算子的編譯效率高,程式設計應儘量使用複合賦值運算。 例7.5 複合賦值運算子"+="過載
#include<iostream>
using namespace std;
class Point
{
public:
Point (){ X=0;Y=0;}
Point (double x,double y){ X=x;Y=y;}
Point& operator += (const Point &); //複合賦值運算子過載函式
void Dispoint() //輸出點座標
{ cout <<"點的位置是:("<<X<<","<<Y<<")"<<endl;}
private:
double X,Y;
}
Point & Point :: operate +=(const Point &d)
{
X+=d.X;
Y+=d.X;
return (*this);
}
int main()
{
Point p1,p2(2,3.4),p3(3,5.6);
cout<<"運算前p1;"
p1.Dispoint();
p1+=p2;
cout<<"運算後p1;"
p1.Dispoint();
return 0;
}
說明:複合賦值運算子也應返回左值,所以返回值是一個引用。
7.2.3 單目運算子過載 內建的自增與自減各有字首和字尾形式。首先要解決字首和字尾,故在過載時的字尾運算元額外增加一個int型形參,編譯時形參被賦值為0,該引數只負責區分字首和字尾。 例7.6自增自減運算子過載
#include<iostream>
using namespace std;
class Increase
{
public :
Increase( int a=0; int b=0);
Increase& operator ++ (); //字首++
Increase operator ++ (int); //字尾++
Increase& operator -- (); //字首--
Increase operator -- (int); //字尾--
private:
X,Y;
};
Increase :: Increase(int a, int b)
{
X=a;
Y=b;
}
Increase& Increase::operator ++ () //定義字首“++”運算
{
X++;
Y++;
return *this;
}
Increase& Increase::operator ++ (int) //定義字尾“++”運算
{
increase t(X,Y);
X++;
Y++;
return t;
}
Increase& Increase::operator -- () //定義字首“--”運算
{
X--;
Y--;
return *this;
}
Increase& Increase::operator -- (int) //定義字尾“--”運算
{
increase t(X,Y);
X--;
Y--;
return t;
}
void Increase::Display () const
{
cout<<"("<<X<<","<<Y<<")"<<endl;
}
int main()
{
Increase object (1,2),temp;
temp=object;
cout<<"temp 初值:";
temp.Display();
temp=++object;
cout<<"temp=++object後temp的值";
temp.Display();
... //包含另外三個函式呼叫
return 0;
}
說明: (1)在定義字尾運算時,不使用形參int,字尾運算返回的是原值,因此要先複製原值再改變,並且只需返回值,不需返回引用。
(2) 字尾運算子定義也可以呼叫字首運算子,字尾“++”定義可改寫為:
Increase Increase :: operator ++ (int)
{
Increase t(X,Y);
++(*this);
return t;
}
(3) 為了和內建型別的定義保持一致,字首運算子返回左值,所以返回值型別的引用。
一般將自增自減運算子過載定義為成員函式。
7.3 虛擬函式 例7.7基類和派生類含有共名成員。
#include<iostream>
using namespace std;
clas Baseclass
{
public:
Baseclass(int a){X=a;}
void Disp()
{
cout<<"x="<<X<<endl;
}
private:
int X;
};
class Derivedclass:public Baseclass
{
public:
Derivedclass(int i,int j):Baseclass(i),Y(j) { } //可以在成員初始化列表中初始化資料成員
void Disp()
{
Baseclass:: Disp();
cout<<"y="<<Y<<endl;
}
private:
int Y;
};
int main()
{
Baseclass base_obj(1);
cout<<"輸出基類物件的成員:"<<endl;
base_obj.Disp();
Derivedclass derived_obj(2,3);
cout<<"輸出派生類物件的成員:"<<endl;
derived_obj.Disp();
return 0;
}
說明:(1)基類和派生類的函式同名型別和引數列表相同,但不會產生二義性,而是同名覆蓋。
(2)通過派生類物件呼叫成員Disp()時,呼叫的是派生類中重寫的Disp()成員函式。
(3)當通過基類指標訪問派生類物件時,呼叫成員函式Disp()是繼承來自基類的成員而非派生類中重寫的成員,與預期不符。C++提供了一種機制,只要將基類中的成員函式宣告為虛擬函式,當通過基類指標訪問派生類物件時,呼叫成員函式Disp()是派生類中重寫的函數了。
7.3.1 虛成員函式 將成員函式宣告為虛擬函式,只要在函式宣告時在函式返回型別前加上關鍵字virtual即可,格式如下: virtual <型別> <函式名> (<引數表>); 例7.9虛成員函式的使用。
#include<iostream>
using namespace std;
clas Baseclass
{
public:
Baseclass(int a){X=a;}
virtual void Disp()
{
cout<<"x="<<X<<endl;
}
private:
int X;
};
class Derivedclass:public Baseclass
{
public:
Derivedclass(int i,int j):Baseclass(i),Y(j) { } //可以在成員初始化列表中初始化資料成員
void Disp()
{
Baseclass:: Disp();
cout<<"y="<<Y<<endl;
}
private:
int Y;
};
int main()
{
Baseclass base_obj(1);
Derivedclass derived_obj(2,3);
Baseclass *basej_p=&base_obj;
cout<<"指標指向基類的物件:"<<endl;
basej_p->Disp();
basej_p=&derived_obj;
cout<<"指標指向派生類物件:"<<endl;
derived_obj->Disp();
return 0;
}
說明: basej_p->Disp()同一條語句執行了不同的程式程式碼,對該函式的聯編是在執行階段,即動態聯編,根據指標所指向物件的型別決定呼叫那個函式,實現了執行多型。
虛擬函式使用注意事項: (1)基類中的虛擬函式與派生類中重寫的同名函式不但要求名字相同,而且返回值和引數表也要相同。否則看出函式過載,即使加上virtual關鍵字,也不會動態繫結。如果基類中的虛擬函式返回一個基類的指標或引用,派生類的虛擬函式也返回一個基類的指標或引用,C++仍可將其視為虛擬函式進行動態繫結。 (2)基類中的虛擬函式在派生類中仍然是虛擬函式,並且可以通過派生一直將這個虛擬函式繼承下去,且不需要加virtual。 (3)虛擬函式的宣告只能出現在類定義時的函式宣告中。 (4)虛擬函式只能是類的成員函式,不能是友元函式,因為虛擬函式只適應於有繼承關係的類的物件。 不能是靜態成員函式,因為基類定義了靜態成員,那麼整個類繼承層次中只能有一個這樣的成員。 不能是行內函數,因為行內函數的程式碼替換是在編譯時完成的。 (5) 只能通過指標或引用來操作虛擬函式實現多型。 (6)設計派生類時,如果不為了實現多型,最好避免與基類使用相同的成員名,避免發生同名覆原則。 7.3.2 虛解構函式 不能宣告虛建構函式,但可以宣告虛解構函式。如果派生類中存在動態記憶體分配,並且記憶體釋放工作是在解構函式中實現的,這時必須將解構函式宣告為虛擬函式。 例7.10 非虛構函式
#include<iostream>
using namespace std;
clas Baseclass
{
public:
Baseclass()
{
cout<<"呼叫基類建構函式"<<endl;
}
virtual void Disp() const
{
cout<<"輸出基類成員"<<endl;
}
~Baseclass()
{
cout<<"呼叫基類解構函式"<<endl;
}
};
class Derivedclass :public Baseclass
{
public:
Derivedclass()
{
cout<<"呼叫派生類建構函式"<<endl;
}
void Disp() const
{
cout<<"輸出派生類成員"<<endl;
}
~Derivedclass()
{
cout<<"呼叫派生類解構函式"<<endl;
}
}
int main()
{
Baseclass *base_p=new Derivedclass();
*base_p->Disp();
delete base_p; //手動釋放派生類剩餘的成員(成員函式)
return 0;
}
程式的執行結果:
呼叫基類建構函式
呼叫派生類建構函式
輸出派生類成員
呼叫基類解構函式
由輸出結果知道,通過基類指標釋放派生類物件時,只調用了基類的解構函式,釋放掉了基類部分,派生類解構函式則沒有呼叫。如果派生類中添加了需要動態記憶體分配的資料成員,那麼當派生類物件被釋放時,只調用基類的解構函式,派生類的解構函式則無法被自動呼叫,派生類資料成員的所佔用的記憶體則沒有被釋放,這將造成記憶體洩漏。 為了防止這種現象,將基類解構函式宣告為虛解構函式。
例7.10 虛構函式
#include<iostream>
using namespace std;
clas Baseclass
{
public:
Baseclass()
{
cout<<"呼叫基類建構函式"<<endl;
}
virtual void Disp() const
{
cout<<"輸出基類成員"<<endl;
}
virtual ~Baseclass()
{
cout<<"呼叫基類解構函式"<<endl;
}
};
class Derivedclass :public Baseclass
{
public:
Derivedclass()
{
cout<<"呼叫派生類建構函式"<<endl;
}
void Disp() const
{
cout<<"輸出派生類成員"<<endl;
}
~Derivedclass()
{
cout<<"呼叫派生類解構函式"<<endl;
}
}
程式的執行結果:
呼叫基類建構函式
呼叫派生類建構函式
輸出派生類成員
呼叫派生類解構函式
呼叫基類解構函式
說明:(1)把基類解構函式宣告為虛擬函式後,通過基類指標釋放其所指的派生類物件時是按照對派生類物件的釋放順序進行的。 (2)如果一個類的解構函式是虛擬函式,那麼它派生類所有的解構函式也是虛擬函式如果派生類類繼續派生下去,這個性質也將一直被繼承。 (3)即使處於繼承最頂層的根基類的解構函式沒有實際工作做,也應該定義一個虛構函式。像其他虛擬函式一樣,解構函式的虛擬函式形狀將被繼承,無論派生類顯示定義還是使用預設解構函式,派生類解構函式都是虛擬函式。
- 7.4抽象類 是一種特殊的類,是為了抽象的目的而建立的,它所描述的是所有派生類的共性,那些高度抽象、無法具體化的共性由純虛擬函式來描述。含有純虛擬函式的類稱為抽象類,一個抽象類至少有一個純虛擬函式。
7.4.1 純虛擬函式 設計基類時,遇到了其成員函式不能被全部實現的情況。例如:不知道具體的圖形,所以計算面積的成員函式難以實現。但為了能夠多型的使用該函式,在基類中還需要定義該成員函式,這時把它定義為純虛擬函式。定義格式如下: virtual <型別> <函式名>(<引數表>)=0; 純虛擬函式不需要定義函式體,其值為0。純虛擬函式的函式體有"=0 "替代。與空函式不同空函式有函式體,由一對花括號闊起來,含有空函式的類可以建立物件。含有純虛擬函式的類不能建立物件,純虛擬函式為派生類提供了一個空位置,為所有派生類提供一個公共介面。派生類中必須對純虛擬函式進行重寫,才能建立物件。 7.4.2抽象類與具體類 含有純虛擬函式的是抽象類,抽象類不能建立物件,只能用作基類,其存在是為了實現執行時多型。定義抽象類是為了建立一個類的通用框架,用於引導建立一系列結構類似的完整派生類,為整個類組提供統一的介面形式。 抽象類雖然不能建立物件,但是可以定義指標和引用。但它們只能指向派生類物件,用來實現執行時多型。 例7.12抽象類的應用—圖形類
#include<iostream>
using namespace std;
const double PI=3.14;
class Shape //圖形類-抽象類
{
public:
virtual double Area()=0;
virtual void Disp()=0;
};
class Circle:public Shape //圓類-具體類
{
public:
Circle(double r){ R=r;}
double Area(){return PI*R*R;}
void Disp(){cout<<"圓半徑:"<<R;}
privae:
double R;
};
class Square:public Shape //正方形類-具體類
{
public:
Square(double a){ A=a; } //純虛擬函式
double Area(){return A*A;} //純虛擬函式
void Disp(){cout<<"正方形邊長:"<<A;}
private:
double A;
};
class Rectangle:public Shape //長方形類-具體類
{
public:
Rectangle(int a,int b){ A=a;B=b; }
double Area(){return A*B;}
void Disp(){cout<<"長方形邊長:"<<A<<"寬:"<<B;}
private:
double A,B;
};
int main()
{
Shape *p;
Circle Circle(1);
Square Square(2);
Rectangle Rectangle(3,4);
p=&Circle;
p->Disp();
cout<<"\t\t 面積是:"<<p->Area()<<endl;
p=□
p->Disp();
cout<<"\t\t 面積是:"<<p->Area()<<endl;
p=&Rectangle;
p->Disp();
cout<<"\t\t 面積是:"<<p->Area()<<endl;
return 0;
}
說明:(1)在程式中,Shape為抽象類,不能建立物件,但可以定義指標。
(2)Area()和Disp()為純虛擬函式,為整個圖形類提供了一個統一的操作介面。在派生類中分別重寫,實現各自的功能。
(3)在執行的不同階段基類指標指向了不同的派生類物件,因此同樣的語句p->Disp()和p->Area()執行了不同的程式碼,實現了執行時多型。
抽象類具有如下特點: (1)抽象類只能做其他類的基類,不能例項化,純虛擬函式由派生類實現。 (2)只有派生類給出基類所以純虛擬函式的實現,這派生類為具體類,可以建立物件。否則仍然為抽象類。 (3)可以定義抽象類指標和引用,但必須使其指向派生類物件,以便多型的使用它們。