C 拾遺 C 虛函式實現原理

2021-07-09 07:07:56 字數 4103 閱讀 9713

我們知道,與c語言相比,c++在布局和訪問時間上的額外開銷主要是由虛函式(virtual function)機制和虛繼承(virtual base class)機制引起的。在前面一篇文章中,我們從記憶體布局的角度入手,分析了虛繼承的實現原理,傳送門:從記憶體布局看c++虛繼承的實現原理。

今天,我們來分析c++的虛函式機制。

在c++中,存在著靜態聯編和動態聯編的區別。簡而言之,「靜態聯編」是指編譯器在編譯過程中就完成了聯編或繫結(binding),比如函式過載,c++編譯器根據傳遞給函式的引數和函式名稱就可以判斷具體要使用哪乙個函式,這種在編譯過程中進行的繫結就稱作靜態聯編(static binding)。而「動態聯編」是指在在程式執行時完成的聯編。

c++通過虛函式(virtual function)機制來支援動態聯編(dynamic binding),並實現了多型機制。多型是物件導向程式設計語言的基本特徵之一。在c++中,多型就是利用基類指標指向子類例項,然後通過基類指標呼叫子類(虛)函式從而實現「乙個介面,多種形態」的效果。

c++利用基類指標和虛函式來實現動態多型,虛函式的定義很簡單,只要在成員函式原型前加上關鍵字virtual即可,並且,virtual關鍵字只要宣告一次,其派生類中的相應函式仍為虛函式,可以省略virtual關鍵字。

下面,我們就通過乙個簡單的例子來展示一下虛函式如何實現多型:

**1:

class base1 

void func2()

};class base2 : public base1

};int main()

輸出如下:

我們看到,同樣是基類指標指向子類物件,對於func1,呼叫了基類的實現版本,對於func2,卻呼叫了子類的實現版本。由此可見

對於virtual函式,具體呼叫哪個版本的函式取決於指標所指向物件型別。

對於非virtual函式,具體呼叫哪個版本的函式取決於指標本身的型別,而和指標所指物件型別無關。

那麼,這一功能是如何實現的呢?下面我們就來**一下虛函式的實現原理。

c++中虛函式是如何實現的呢?不少資料中都提到過,c++通過虛函式表和虛函式表指標來實現virtual function機制,具體而言:

為了更加直觀地了解上面描述的實現機制,我們通過檢視帶有virtual function的類的記憶體布局來證實一下。關於如何檢視c++類的記憶體布局,可以參考我之前寫的一篇文章傳送門。

我們先定義乙個類base:

**2:

class base

// 虛函式func2

virtual

void func2()

// 虛函式func3

virtual

void func3()

int a;

};

使用visual studio的命令列選項,我們可以看到類base的記憶體布局如下所示:

我們可以看到在base類的記憶體布局上,第乙個位置上存放虛函式表指標,接下來才是base的成員變數。另外,存在著虛函式表,該表裡存放著base類的所有virtual函式。

我們用一幅圖來展示一下base類的記憶體布局:

在上圖中,虛函式表中存在在乙個「結束結點」,用以標識虛函式表的結束(具體實現與編譯器有關)。

既然虛函式表指標通常放在物件例項的最前面的位置,那麼我們應該可以通過**來訪問虛函式表:

**3:

int main()

在visual studio2013 + window10中執行結果如下:

在**3中,我們把&b強轉為int *,這樣就取到了虛函式表的位址,然後,再次取址,得到了第乙個虛函式位址。同樣,我們通過對函式指標進行下標操作就可以進一步得到第二和第三個虛函式位址。

通過上面例項演示,我們了解了虛函式具體的實現機制。那c++又是如何利用基類指標和虛函式來實現多型的呢?這是,我們就需要**一下在繼承環境下虛函式表示如何工作的。

我們先來看看簡單的單繼承情況。假設存在下面的兩個類base和a,a類繼承自base類:

**4:

class base

// 虛函式func2

virtual

void func2()

// 虛函式func3

virtual

void func3()

int a;

};class a : public base

void func2()

// 新增虛函式func4

virtual

void func4()

};

我們繼續利用visual studio提供的命令列工具檢視一下這兩個類的記憶體布局。base類已經在上面展示過,為了便於比較,這裡再貼一次:

類base的記憶體布局圖:

類a的記憶體布局圖:

另外,我們注意到,類a和類base中都只有乙個vfptr指標,前面我們說過,該指標指向虛函式表,我們分別輸出類a和類base的vfptr:

**5:

int main()

輸出資訊如下:

我們可以看到,類a和類b分別擁有自己的虛函式表指標vptr和虛函式表vtbl。到這裡,你是否已經明白為什麼指向子類例項的基類指標可以呼叫子類(虛)函式?每乙個例項物件中都存在乙個vptr指標,編譯器會先取出vptr的值,這個值就是虛函式表vtbl的位址,再根據這個值來到vtbl中呼叫目標函式。所以,只要vptr不同,指向的虛函式表vtbl就不同,而不同的虛函式表中存放著對應類的虛函式位址,這樣就實現了多型的」效果「。

最後,我們用一幅圖來表示單繼承下的虛函式實現:

假設存在下面這樣的四個類:

**6:

class base

// 虛函式func2

virtual

void func2()

// 虛函式func3

virtual

void func3()

};class a : public base

void func2()

};class b : public base

void func2()

};class c : public a, public b

void func2()

};

在**6中,類a和類b分別繼承自類base,類c繼承了類b和類a,我們檢視一下類c的記憶體布局:

我們可以看到,類c中擁有兩個虛函式表指標vptr。類c中覆蓋了類a的兩個同名函式,在虛函式表中體現為對應位置替換為c中新函式;類c中覆蓋了類b中的兩個同名函式,在虛函式表中體現為對應位置替換為c中新函式(注意,這裡使用跳轉語句,而不是重複定義)。

類c的記憶體布局可以歸納為下圖:

C 拾遺 函式過載

c 拾遺 函式過載 關於作用域,需要指出幾點事實 用大括號 括起來的區域處於同一作用域,常見的有函式體 for if語句等。同一作用域內不可出現同名的變數,若是函式同名,那就是函式過載問題。不同作用域內同名與否,沒影響。所有的函式之外的區域就是全域性作用域。首先需要指出,同一作用域中的函式才會出現過...

C 拾遺(二 函式)

1.引數陣列。c 的特色,允許函式引數的最後指定乙個引數陣列,可以使用個數不定的引數呼叫,用params關鍵字定義 static double sumvals params double vals return sum 呼叫sumvals 1,2,3 2.值引數和引用引數。引用引數使用關鍵字ref指...

C語言拾遺

main函式引數 c語言規定main函式引數只能有兩個,習慣上這兩個引數寫成argc和argv。c語言還規定argc必須是整形變數,argv必須是指向字串的指標陣列。因此,main函式的函式頭應該寫為 main argc,argv int argc char argv 或者 main int arg...