1. 程式人生 > >關於C++多型性的一些總結

關於C++多型性的一些總結

    在任何一門面向物件的程式語言中,多型性(polymorphism)都是非常重要的一個概念。在面向物件的三大元素中,封裝使程式碼的模組化變得簡單,繼承則可以擴充套件已有的程式碼,而多型則是為了滿足介面的重用。所謂的多型,通俗的來講,其實就是讓不同的物件在接受到相同的訊息時能夠做出不同的反應,好像有多個型別一般,這體現了物件的自恰性。打個比方,兩軍對陣,同樣是鳴鑼,可能一方軍隊在聽到鑼聲時會採取撤退的行動;而另一方的軍隊聽到鑼聲後,則可能會採取發動進攻的行動。關鍵在於雙方軍隊各自都有對聽到鑼聲這一訊號而採取什麼樣的行動有所對應。多型也是一樣。在滿足繼承關係的兩個類中,只要在基類中定義了一個虛擬函式,且子類中對該虛擬函式提供了有效的覆蓋版本,那麼在通過指向子類的基類指標或引用子類的基類引用呼叫該函式時,會由呼叫物件的具體型別來確定呼叫哪個物件的函式,這種現象稱之為多型(執行期多型)。在C++中,多型性的實現有兩種形式.——動態的多型性(執行時的多型性)和靜態的多型性(編譯時的多型性),要麼在執行時決定,要麼在編譯時決定。其中動態的多型性正是通過虛擬函式表來實現的。下面先讓我們來看一段程式碼(執行環境:64位Ubuntu 14.04 LTS + gcc 4.8.2):

#include <iostream>
using namespace std;
class Shape{
public:
	Shape(void){}
	virtual void show(void){
		cout << “Shape” << endl;
	}
	virtual void foo(void){
		cout << “Shape::foo” << endl;
	}
};
class Circle:public Shape{
public:
	Circle(void){}
	void show(void){
		cout << “Circle” << endl;
	}
};
class Rectangle:public Shape{
public:
	Rectangle(void){}
	void show(void){
		cout << “Rectangle” << endl;
	}
};
int main(void){
	Circle c;
	Rectangle r;
	Shape* p = &c;
	p->show();			//Cricle
	p = &r;
	p->show();			//Rectangle
	Shape& pr = c;
	pr.show();			//Circle
	Shape& pt = r;
	pt.show();			//Rectangle
	return 0;
}

從這幅圖中可以看出,在Shape物件中有一個虛表指標(_vptr_Shape),指向了Shape中的虛擬函式表,而虛擬函式表高地址部分內容中有兩個函式指標,分別指向了Shape基類中的show函式和foo函式。整個多型的過程是這樣的:在例項化Circle物件時會先例項化Shape子物件,這時虛表指標指向了Shape類中的虛擬函式表。當Shape子物件構建完成後會去構建Circle的獨有部分,這個時候會將虛表指標的值改為Circle類的虛擬函式表的地址。由於Circle提供了對show函式的有效覆蓋,而沒有提供對foo函式的有效覆蓋,所以在Circle物件的虛表指標(_vptr_Circle)所指向的虛擬函式表中,Circle::show函式代替了Shape::show函式,而foo函式仍然為Shape::foo函式。當我們使用一個指向子類的基類指標或引用子類的基類引用時,作業系統會先根據指標所指向的物件找到虛表指標,再根據虛表指標找到對應的類中的虛擬函式表,再來呼叫相應的函式,這就是通過虛擬函式表來實現執行時的多型的原理
要實現執行時多型必須滿足以下幾個條件:
1.在基類中定義了虛擬函式且子類中提供了對基類虛擬函式的有效覆蓋
2.必須通過指向子類的基類指標或引用子類的基類引用來呼叫
3.只有成員函式形式的運算子過載才能實現多型性,全域性函式形式的運算子過載無法實現多型
4.呼叫虛擬函式的指標也有可能是基類的this指標,同樣滿足多型的條件,但在建構函式和解構函式中除外


有效的覆蓋(override)應滿足的條件:
1.必須是使用virtual宣告的成員函式,不能是靜態成員函式或全域性函式
2.覆蓋版本中必須帶有和基類虛擬函式中完全相同的函式簽名,即函式名,形參表和常屬性
3.如果基類中的虛擬函式的返回型別是基本資料型別,那麼子類中的覆蓋版本必須返回相同的基本資料型別;如果基類中的虛擬函式返回的是類型別的指標或引用,那麼允許子類中的覆蓋版本返回其子類型別的指標或引用
4.如果基類版本的虛擬函式帶有異常說明,那麼子類覆蓋版本不能說明比基類版本的虛擬函式更多的異常說明(可以少不可以多)
5.無論基類版本位於基類中的public,private,protected部分中,子類中的覆蓋版本可以出現在子類中包括public,private,protected的任何部分

雖然執行時多型使用形式簡單,但是它會給程式帶來一定的開銷,對效能的影響主要體現在以下幾點:
1.虛擬函式表本身會增加記憶體空間的開銷
2.與普通函式相比,虛擬函式的呼叫要多出幾個步驟,增加了執行時間的開銷
3.動態繫結會妨礙編譯器來通過內聯優化程式碼(最突出)

既然動態多型性(執行時多型)是通過虛擬函式表來實現,那麼靜態的多型性(編譯時多型)則是通過什麼來實現的呢?先來看一下下面的 程式碼(執行環境:64位Ubuntu 14.04 LTS + gcc 4.8.2):

#include <iostream>
using namespace std;
class Circle{
public:
	void show(void){	
		cout << ”Circle” << endl;
	}
};
class Rectangle{
public:
	void show(void){
		cout << ”Rectangle” << endl;
	}
};
template<typename Shape>
void drawAny(Shape& shape){
	shape.show();
}
int main(void){
	Circle c;
	Rectangle r;
	drawAny(r);
	drawAny(c);
	return 0;
}    
執行結果如下:
Rectangle
Circle
從上面程式碼執行結果可以看出,我們的drawAny實現了多型。在面對同樣的函式呼叫時,我們通過傳遞不同的引數來實現不同的函式呼叫。這種多型性稱之為編譯期多型性,這是因為在編譯的過程中,編譯器在看到函式模板定義的時候還不知道其型別,這時編譯期會先忽略型別,做一些跟型別無關的語法檢查,然後生成一個內部表示。當編譯器看到對該函式模板的呼叫時,便會結合具體的引數型別生成相應函式的二進位制程式碼。也就是說,在上面的那個例子中,我們利用了函式模板的隱式推斷,將我們要呼叫的類型別物件通過引用的方式傳遞給了函式模板,編譯器通過對引數型別的確認,確定了被呼叫的函式所屬的物件,進而確定了呼叫的是哪個具體函式,實現了多型。這種方法相對於通過使用虛擬函式表實現的執行時多型,實現較為複雜,不如虛擬函式簡單易用,但是它的執行效率要比通過使用虛擬函式表實現的多型方式要高。
除此之外,我們也可以使用函式過載來實現多型,這一樣也是屬於編譯期多型。來看一段程式碼(執行環境:64位Ubuntu 14.04 LTS + gcc 4.8.2):
#include <iostream>
using namespace std;
class Circle{
public:
	void show(void){
		cout << ”Circle” << endl;
	}
};
class Rectangle{
public:
	void show(void){
		cout << ”Rectangle” << endl;
	}
};
void drawAny(Circle & circle){
	circle.show();
}
void drawAny(Rectangle & rect){
	rect.show();
}

int main(void){
	Circle c;
	Rectangle r;
	drawAny(r);
	drawAny(c);
	return 0;
}
執行結果如下:
Rectangle
Circle
由上面程式碼可以看出,使用函式過載同樣也可以實現多型性。面對同樣的函式呼叫,都能得到不同的正確結果。這種方法實現起來非常簡單,但是過程繁複,而且不具備可擴充套件性。程式中的需要實現多型特性的類個數少的話還行,但如果多呢?可複用性和可擴充套件性都太差,一般都不建議使用。在這裡簡單的提一下過載(overload)、覆蓋(override)和隱藏()的區別:在同一作用域中,函式名相同,引數表不同,則構成過載關係;在具有繼承關係的父子類中,帶有virtual關鍵字,且函式簽名完全相同,則構成覆蓋關係;若不在同一作用域中,函式名相同,則構成隱藏關係
總結:
1.使用虛擬函式實現多型時,由於編譯器內建的支援,使用起來較為簡單,而使用模板實現多型較為複雜,使用函式過載實現多型則非常簡單,但是缺乏可複用性和可擴充套件性。
2.使用執行時多型會增加程式的開銷,而編譯期多型對程式的開銷幾乎沒有影響。
3.使用模板來實現的多型無法通過基類物件指標陣列來實現對多個不同子類物件的多型操作,而使用虛擬函式可以達到這一目標