程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++入門學習——虛函數表介紹

C++入門學習——虛函數表介紹

編輯:C++入門知識

C++入門學習——虛函數表介紹


多態

多態是指使用相同的函數名來訪問函數不同的實現方法,可以簡單概括為“一種接口,多種方法”。


C++支持編譯時多態(也叫靜態多態)和運行時多態(也叫動態多態),運算符重載和函數重載就是編譯時多態,而派生類和虛函數實現運行時多態。

靜態多態與動態多態的實質區別就是函數地址是早綁定還是晚綁定。如果函數的調用,在編譯器編譯期間就可以確定函數的調用地址,並生產代碼,是靜態多態(編譯時多態),就是說地址是早綁定的。而如果函數調用的地址不能在編譯器期間確定,需要在運行時才確定,這就屬於晚綁定,是動態多態(運行時多態)。

這裡,我們探究的是動態多態。

C++動態多態性是通過虛函數來實現的,虛函數允許子類(派生類)重新定義父類(基類)成員函數,而子類(派生類)重新定義父類(基類)虛函數的做法稱為覆蓋(override),或者稱為重寫。

最常見的用法是:父類(基類)指針,基類指針可以指向任何子類(派生類)對象(實例),然後通過基類的指針調用實際派生類的成員函數,示例如下:

 

#include 
using namespace std;
 
class Base 
{ 
public: 
	void f(int x){ cout << Base::f(int)  << x << endl; } 
	void f(float x){ cout << Base::f(float)  << x << endl; }
	// 必須有virtual關鍵字,此為虛函數
	virtual void g(void){ cout << Base::g(void) << endl;} 
}; 
class Derive: public Base 
{ 
public: 
	// virtual關鍵字,可有可無,此為虛函數
	virtual void g(void){ cout << Derived::g(void) << endl;} 
}; 

int main(void) 
{ 
	Derive d; 
	Base *pb = &d; 
	pb->f(42);     // 運行結果: Base::f(int) 42 
	pb->f(3.14f);  // 運行結果: Base::f(float) 3.14 
	pb->g();       // 運行結果: Derived::g(void) 
	
	return 0;
} 

虛函數表

 

C++ 中的虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實現的,簡稱為V-Table。每個含有虛函數的類有一張虛函數表,表中每一項是一個虛函數的地址, 也就是說,虛函數表的每一項是一個虛函數的指針。 沒有虛函數的C++類,是不會有虛函數表的。

 

/

/

 

下面通過一些示例簡單介紹虛函數表:

 

#include 
using namespace std;
 
class Base 
{
public:
	virtual void f() { cout << Base::f << endl; }
	virtual void g() { cout << Base::g << endl; }
	virtual void h() { cout << Base::h << endl; }
 
};

int main(void) 
{ 
	Base b;
	b.f(); //Base::f
	b.g(); //Base::g
	b.h(); //Base::h
	
	return 0;
} 

Base的實例(即對象b)的虛函數表如下:

 

/

注意:在上面這個圖中,虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符“”一樣,其標志了虛函數表的結束。這個結束標志的值在不同的編譯器下是不同的。

 

無虛函數覆蓋的虛函數表

下面,再讓我們來看看繼承時的虛函數表是什麼樣的。


假設有如下所示的一個繼承關系:

 

#include 
using namespace std;
 
class Base 
{
public:
	virtual void f() { cout << Base::f << endl; }
	virtual void g() { cout << Base::g << endl; }
	virtual void h() { cout << Base::h << endl; }
 
};

class Derive:public Base
{
public:
	virtual void f1() { cout << Derive::f1 << endl; }
	virtual void g1() { cout << Derive::g1 << endl; }
	virtual void h1() { cout << Derive::h1 << endl; }
		
};

int main(void) 
{ 
	Derive d; //派生類對象
	
	return 0;
}

請注意,在這個繼承關系中,子類沒有重載任何父類的函數。那麼,在派生類的實例(Derive d)中,其虛函數表如下所示:

 

/

無虛函數覆蓋的虛函數表特點如下:

1)虛函數按照其聲明順序放於表中。

2)父類(派生類)的虛函數在子類(基類)的虛函數前面。

 

有虛函數覆蓋的虛函數表

沒有覆蓋父類的虛函數是毫無意義的。之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。

 

下面,我們來看一下,如果子類中有重載了父類的虛函數,會是一個什麼樣子?

 

#include 
using namespace std;
 
class Base 
{
public:
	virtual void f() { cout << Base::f << endl; }
	virtual void g() { cout << Base::g << endl; }
	virtual void h() { cout << Base::h << endl; }
 
};

class Derive:public Base
{
public:
	//重寫父類的f()虛函數
	virtual void f() { cout << Derive::f << endl; }
	virtual void g1() { cout << Derive::g1 << endl; }
	virtual void h1() { cout << Derive::h1 << endl; }
		
};

int main(void) 
{ 
	Base *p = NULL; //父類指針
	Base b;		//父類對象
	Derive d;	//子類對象
	
	p = &b; //父類指針指向父類對象
	p->f(); //運行結果:Base::f
	
	p = &d; //父類指針指向子類對象
	p->f(); //運行結果:Derive::f
	
	return 0;
}

為了讓大家看到被繼承過後的效果,在這個類的設計中,只覆蓋了父類的一個虛函數:f()。那麼,對於派生類的實例,其虛函數表會是下面的一個樣子:

 

/

有虛函數覆蓋的虛函數表特點如下:

1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。

2)沒有被覆蓋的函數依舊。

 

本例中:

Base *p = NULL; //父類指針

p = &d; //父類指針指向子類對象

p->f(); //運行結果:Derive::f

 

由 p(即 &d)所指的內存中的父類虛函數表的 f() 的位置已經被實際子類Derive::f()函數地址所取代,於是在實際調用發生時,是 Derive::f() 被調用了。這就實現了多態。

 

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved