《C++面向物件程式設計》課程筆記 lessen6
1. 虛擬函式和多型的基本概念
1 虛擬函式
在類的定義中,前面有 virtual 關鍵字的成員函式就是虛擬函式。
class base
{
virtual int get();
};
int base::get() { }
- virtual 關鍵字只用在類定義裡的函式宣告中,寫函式體時不用。
- 建構函式和靜態成員函式不能是虛擬函式。
2 多型
多型的表現形式一:派生類的指標可以賦值給基類指標。
通過基類指標呼叫基類和派生類中的同名虛擬函式時:
- 若該指標指向一個基類的物件,那麼被呼叫的是基類的虛擬函式。
- 若該指標指向一個派生類的物件,那麼被呼叫的是派生類的虛擬函式。
這種機制就叫做“多型”。
#include <iostream> using namespace std; class CBase { public: virtual void SomeVirtualFunction() { cout<<"base"<<endl; } }; class CDerived:public CBase { public: virtual void SomeVirtualFunction() { cout<<"derived"<<endl; } }; int main() { CDerived ODerived; CBase *p = &ODerived; p->SomeVirtualFunction(); //基類指標指向了派生類物件,因此呼叫派生類的虛擬函式 system("pause"); return 0; }
多型的表現形式二:派生類的物件可以賦值給基類的引用。
通過基類引用呼叫基類和派生類中的同名虛擬函式時:
- 若該引用引用的是一個基類的物件,那麼被呼叫的是基類的虛擬函式。
- 若該引用引用的是一個派生類的物件,那麼被呼叫的是派生類的虛擬函式。
這種機制也叫做“多型”。
int main()
{
CDerived ODerived;
CBase & r = ODerived;
r.SomeVirtualFunction(); //基類引用引用的是派生類物件,因此呼叫派生類的虛擬函式
system("pause");
return 0;
}
多型的作用:在面向物件的程式設計中使用多型,能夠增強程式的可擴充性,即程式需要修改或增加功能的時候,需要改動和增加的程式碼較少。
3 多型使用的例項:遊戲《魔法門之英雄無敵》
- 遊戲中有很多種怪物,每種怪物都有一個類與之對應,每個怪物就是一個物件。
- 怪物能夠互相攻擊,攻擊敵人和被攻擊時都有相應的動作,動作是通過物件的成員函式實現的。
- 為每個怪物類編寫 Attack、FightBack 和 Hurted 成員函式。
- Attack 函式表現攻擊動作,攻擊某個怪物,並呼叫被攻擊怪物的 Hurted 函式,以減少被攻擊怪物的生命值,同時也呼叫被攻擊怪物的 FightBack 函式,遭受被攻擊怪物的反擊。
- Hurted 函式減少自身生命值,並表現受傷動作。
- FightBack 函式表現反擊動作,並呼叫被反擊物件的 Hurted 函式,使被反擊物件受傷。
- 設定基類 CCreature,並且使 CDragon,CWolf 等其它類都從 CCreature 派生而來。
非多型實現方法:
class CCreature
{
protected:
int nPower; //代表攻擊力
int nLifeValue; //代表生命值
};
class CDragon:public CCreature
{
public:
void Attack(CWolf * pWolf)
{
//...表現攻擊動作的程式碼
pWolf->Hurted(nPower);
pWolf->FightBack(this);
}
void Attack(CGhost * pGhost)
{
//...表現攻擊動作的程式碼
pGhost->Hurted(nPower);
pGhost->FightBack(this);
}
void Hurted(int nPower)
{
//... 表現受傷動作的程式碼
nLifeValue -=nPower;
}
void FightBack(CWolf * pWolf)
{
//...表現反擊動作的程式碼
pWolf->Hurted(nPower/2);
}
void FightBack(CGhost * pGhost)
{
//...表現反擊動作的程式碼
pGhost->Hurted(nPower/2);
}
//有 n 種怪物,CDragon 類中就會有 n 個 Attack 成員函式,以及 n 個 FightBack 成員函式,對於其他類也如此。
};
如果遊戲版本升級,增加了新的怪物雷鳥 CThunderBird,則程式改動較大。
所有的類都需要增加兩個成員函式:
void Attack(CThunderBird * pThunderBird);
void FightBack(CThunderBird * pThunderBird);
多型的實現方法:
//多型的實現方法
class CCreature
{
protected:
int m_nPower; //代表攻擊力
int m_nLifeValue; //代表生命值
public:
virtual void Attack(CCreature * pCreature) { }
virtual void Hurted(int nPower) { }
virtual void FightBack(CCreature * pCreature) { }
//基類只有一個 Attack 成員函式,也只有一個 FightBack 成員函式;所有 CCreature 的派生類也這樣。
};
//以CDragon派生類為例
class CDragon:public CCreature
{
public:
virtual void Attack(CCreature * pCreature);
virtual void Hurted(int nPower);
virtual void FightBack(CCreature * pCreature);
};
void CDragon::Attack(CCreature * pCreature)
{
//...表現攻擊動作的程式碼
pCreature->Hurted(m_nPower); //多型。pCreature指標指向哪個物件,就呼叫哪個物件的Hurted 虛擬函式
pCreature->FightBack(this); //多型
}
void CDragon::Hurted(int nPower)
{
//... 表現受傷動作的程式碼
m_nLifeValue -=nPower;
}
void CDragon::FightBack(CCreature * pCreature)
{
//...表現反擊動作的程式碼
pCreature->Hurted(m_nPower/2); //多型
}
int main()
{
CDragon Dragon;
CWolf Wolf;
CGhost Ghost;
CThunderBird ThunderBird;
Dragon.Attack(& Wolf);
Dragon.Attack(& Ghost);
Dragon.Attack(& ThunderBird); //派生類指標可以賦值給基類指標
}
如果遊戲版本升級,增加了新的怪物雷鳥 CThunderBird。只需要編寫新類 CThunderBird,不需要在已有的類裡專門為新怪物增加:void Attack(CThunderBird * pThunderBird); void FightBack(CThunderBird * pThunderBird); 成員函式,已有的類可以原封不動。
4 多型使用的例項:幾何形體處理程式
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class CShape
{
public:
virtual double Area() = 0; //純虛擬函式(因為沒有一種形狀為 Shape 型別)
virtual void PrintInfo() = 0;
};
class CRectangle:public CShape
{
public:
int w,h;
virtual double Area();
virtual void PrintInfo();
};
double CRectangle::Area()
{
return w * h;
}
void CRectangle::PrintInfo()
{
cout<<"Rectangle:"<<Area()<<endl;
}
class CCircle:public CShape
{
public:
int r;
virtual double Area();
virtual void PrintInfo();
};
double CCircle::Area()
{
return 3.14 * r * r;
}
void CCircle::PrintInfo()
{
cout<<"Circle:"<<Area()<<endl;
}
class CTriangle:public CShape
{
public:
int a,b,c;
virtual double Area();
virtual void PrintInfo();
};
double CTriangle::Area()
{
double p = (a+b+c)/2.0;
return sqrt(p*(p-a)*(p-b)*(p-c));
}
void CTriangle::PrintInfo()
{
cout<<"Triangle:"<<Area()<<endl;
}
CShape * pShapes[100];
int MyCompare(const void * s1, const void * s2)
{
double a1,a2;
CShape ** p1; //s1,s2是 void * ,不可寫“* s1”來取得 s1 指向的內容
CShape ** p2;
p1 = (CShape **)s1; //s1,s2 指向pShapes陣列中的元素,陣列元素的型別是CShape * 。
p2 = (CShape **)s2; //故p1,p2 都是指向指標的指標,型別為 CShape ** 。
a1 = (*p1)->Area(); //*p1 的型別是 CShape *,是基類指標,故此句為多型
a2 = (*p2)->Area();
if (a1<a2)
{
return -1;
}
else if(a2<a1)
return 1;
else
return 0;
}
int main()
{
int i;
int n;
CRectangle * pr;
CCircle * pc;
CTriangle * pt;
cin>>n;
for (i=0;i<n;i++)
{
char c;
cin>>c;
switch(c)
{
case 'R':
pr = new CRectangle;
cin>> pr->w >> pr->h;
pShapes[i] = pr;
break;
case 'C':
pc = new CCircle;
cin>> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle;
cin>> pt->a >> pt->b >> pt->c;
pShapes[i] = pt;
break;
}
}
qsort(pShapes,n,sizeof(CShape*),MyCompare);
for(i=0;i<n;i++)
pShapes[i]->PrintInfo();
system("pause");
return 0;
}
如果新增新的幾何形體,比如五邊形,則只需要從 CShape 派生出 CPentagon,以及在 main 中的 switch 語句中增加一個 case,其餘部分不變。
用基類指標陣列存放指向各種派生類物件的指標。然後遍歷該陣列,就能對各個派生類物件做各種操作。這是很常用的做法。
5 多型使用的例項三
#include <iostream>
using namespace std;
class Base
{
public:
void fun1() { fun2();} //等價於 this->fun2(),this 是基類指標,fun2 是虛擬函式,所以是多型(基類指標呼叫虛擬函式)
virtual void fun2() { cout<<"Base::fun2()"<<endl;}
};
class Derived:public Base
{
public:
virtual void fun2() { cout<<"Derived::fun2()"<<endl;}
};
int main()
{
Derived d;
Base * pBase = & d;
pBase ->fun1(); //該句不是多型 //輸出:Derived::fun2()
system("pause");
return 0;
}
- 在非建構函式、非解構函式的成員函式中呼叫虛擬函式,也是多型。
- 在建構函式和解構函式中呼叫虛擬函式,不是多型。編譯時即可確定,呼叫的函式是自己的類或基類中定義的函式,不會等到執行時才決定呼叫自己的還是派生類的函式。
- 派生類中和基類中虛擬函式同名同參數表的函式,不加 virtual 也自動成為虛擬函式。
2. 多型的實現原理
多型實現的關鍵:虛擬函式表
每一個有虛擬函式的類(或有虛擬函式的類的派生類)都有一個虛擬函式表,該類的任何物件中都放著虛擬函式表的指標。虛擬函式表中列出了該類的虛擬函式地址。多出來的4個位元組就是用來放虛擬函式表的地址的。
多型的函式呼叫語句被編譯成一系列根據基類指標所指向的(或基類引用所引用的)物件中存放的虛擬函式的地址,在虛擬函式表中查詢虛擬函式地址,並呼叫虛擬函式的指令。
3. 虛解構函式、純虛擬函式和抽象類
1 虛解構函式
- 通過基類的指標刪除派生類物件時,通常情況下只調用基類的解構函式。
- 但是,刪除一個派生類的物件時,應該先呼叫派生類的解構函式,然後呼叫基類的解構函式。
解決辦法:把基類的解構函式宣告為 virtual(虛擬函式)
- 派生類的解構函式可以 virtual 不進行宣告
- 通過基類的指標刪除派生類物件時,首先呼叫派生類的解構函式,然後呼叫基類的解構函式。
一般來說,一個類如果定義了虛擬函式,則應該將解構函式也定義成虛擬函式。或者,一個類打算作為基類使用,也應該將解構函式定義成虛擬函式。
注意:不允許以虛擬函式作為建構函式。
2 純虛擬函式和抽象類
- 純虛擬函式:沒有函式體的虛擬函式
virtual void print() = 0; //純虛擬函式
virtual void print() { }; //非純虛擬函式,雖然函式體為空,但是有函式體
- 抽象類:包含純虛擬函式的類
- 抽象類只能作為基類來派生新類使用,不能建立抽象類的物件。(如之前的 CShape 類就是抽象類,沒有shape 這種形狀,所以只能作為基類,無法建立這種形狀的物件)
- 抽象類的指標和引用可以指向由抽象類派生出來的類的物件
A a ; //錯,A是抽象類,不能建立物件
A * pa ; //ok。可以定義抽象類的指標和引用
pa = new A ; //錯誤,A是抽象類,不能建立物件
- 在抽象類的成員函式內可以呼叫純虛擬函式(多型),而在建構函式和解構函式內不能呼叫純虛擬函式(不是多型)。
- 如果一個類從抽象類派生而來,那麼當且僅當它實現了基類中的所有純虛擬函式,它才能成為非抽象類。