1. 程式人生 > >《C++面向物件程式設計》課程筆記 lessen6

《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是抽象類,不能建立物件
  • 在抽象類的成員函式內可以呼叫純虛擬函式(多型),而在建構函式和解構函式內不能呼叫純虛擬函式(不是多型)。
  • 如果一個類從抽象類派生而來,那麼當且僅當它實現了基類中的所有純虛擬函式,它才能成為非抽象類。