C++中的虛函數(shù)的作用主要是實現(xiàn)了多態(tài)的機制。關(guān)于多態(tài),簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調(diào)用實際子類的成員函數(shù)。這種技術(shù)可以讓父類的指針有“多種形態(tài)”,這是一種泛型技術(shù)。所謂泛型技術(shù),說白了就是試圖使用不變的代碼來實現(xiàn)可變的算法。比如:模板技術(shù),RTTI技術(shù),虛函數(shù)技術(shù),要么是試圖做到在編譯時決議,要么試圖做到運行時決議。

關(guān)于虛函數(shù)的使用方法,我在這里不做過多的闡述。大家可以看看相關(guān)的C++的書籍。在這篇文章中,我只想從虛函數(shù)的實現(xiàn)機制上面為大家 一個清晰的剖析。

當然,相同的文章在網(wǎng)上也出現(xiàn)過一些了,但我總感覺這些文章不是很容易閱讀,大段大段的代碼,沒有圖片,沒有詳細的說明,沒有比較,沒有舉一反三。不利于學(xué)習(xí)和閱讀,所以這是我想寫下這篇文章的原因。也希望大家多給我提意見。

言歸正傳,讓我們一起進入虛函數(shù)的世界。

虛函數(shù)表

對C++ 了解的人都應(yīng)該知道虛函數(shù)(Virtual Function)是通過一張?zhí)摵瘮?shù)表(Virtual Table)來實現(xiàn)的。簡稱為V-Table。 在這個表中,主是要一個類的虛函數(shù)的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應(yīng)實際的函數(shù)。這樣,在有虛函數(shù)的類的實例中這個表被分配在了 這個實例的內(nèi)存中,所以,當我們用父類的指針來操作一個子類的時候,這張?zhí)摵瘮?shù)表就顯得由為重要了,它就像一個地圖一樣,指明了實際所應(yīng)該調(diào)用的函數(shù)。

這里我們著重看一下這張?zhí)摵瘮?shù)表。在C++的標準規(guī)格說明書中說到,編譯器必需要保證虛函數(shù)表的指針存在于對象實例中最前面的位置(這是為了保證正確取到虛函數(shù)的偏移量)。 這意味著我們通過對象實例的地址得到這張?zhí)摵瘮?shù)表,然后就可以遍歷其中函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)。

聽我扯了那么多,我可以感覺出來你現(xiàn)在可能比以前更加暈頭轉(zhuǎn)向了。 沒關(guān)系,下面就是實際的例子,相信聰明的你一看就明白了。

假設(shè)我們有這樣的一個類:

class Base {

public:

virtual void f() { cout << "Base::f" << endl; }

virtual void g() { cout << "Base::g" << endl; }

virtual void h() { cout << "Base::h" << endl; }

};

按照上面的說法,我們可以通過Base的實例來得到虛函數(shù)表。 下面是實際例程:

typedef void(*Fun)(void);

Base b;

Fun pFun = NULL;

cout << "虛函數(shù)表地址:" << (int*)(&b) << endl;

cout << "虛函數(shù)表 — 第一個函數(shù)地址:" << (int*)*(int*)(&b) << endl;

// Invoke the first virtual function

pFun = (Fun)*((int*)*(int*)(&b));

pFun();

實際運行經(jīng)果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)

虛函數(shù)表地址:0012FED4

虛函數(shù)表 — 第一個函數(shù)地址:0044F148

Base::f

通過這個示例,我們可以看到,我們可以通過強行把&b轉(zhuǎn)成int *,取得虛函數(shù)表的地址,然后,再次取址就可以得到第一個虛函數(shù)的地址了,也就是Base::f(),這在上面的程序中得到了驗證(把int* 強制轉(zhuǎn)成了函數(shù)指針)。通過這個示例,我們就可以知道如果要調(diào)用Base::g()和Base::h(),其代碼如下:

(Fun)*((int*)*(int*)(&b)+0); // Base::f()

(Fun)*((int*)*(int*)(&b)+1); // Base::g()

(Fun)*((int*)*(int*)(&b)+2); // Base::h()

這個時候你應(yīng)該懂了吧。什么?還是有點暈。也是,這樣的代碼看著太亂了。沒問題,讓我畫個圖解釋一下。如下所示:

注意:在上面這個圖中,我在虛函數(shù)表的最后多加了一個結(jié)點,這是虛函數(shù)表的結(jié)束結(jié)點,就像字符串的結(jié)束符“\0”一樣,其標志了虛函數(shù)表的結(jié)束。這個結(jié)束標志的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛函數(shù)表,如果值是0,表示是最后一個虛函數(shù)表。

下面,我將分別說明“無覆蓋”和“有覆蓋”時的虛函數(shù)表的樣子。沒有覆蓋父類的虛函數(shù)是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內(nèi)部的具體實現(xiàn)。

一般繼承(無虛函數(shù)覆蓋)

下面,再讓我們來看看繼承時的虛函數(shù)表是什么樣的。假設(shè)有如下所示的一個繼承關(guān)系:

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

對于實例:Derive d; 的虛函數(shù)表如下:

我們可以看到下面幾點:

1)虛函數(shù)按照其聲明順序放于表中。

2)父類的虛函數(shù)在子類的虛函數(shù)前面。

我相信聰明的你一定可以參考前面的那個程序,來編寫一段程序來驗證。

一般繼承(有虛函數(shù)覆蓋)

覆蓋父類的虛函數(shù)是很顯然的事情,不然,虛函數(shù)就變得毫無意義。下面,我們來看一下,如果子類中有虛函數(shù)重載了父類的虛函數(shù),會是一個什么樣子?假設(shè),我們有下面這樣的一個繼承關(guān)系。

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

我們從表中可以看到下面幾點,

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

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

這樣,我們就可以看到對于下面這樣的程序,

Base *b = new Derive();

b->f();

由b所指的內(nèi)存中的虛函數(shù)表的f()的位置已經(jīng)被Derive::f()函數(shù)地址所取代,于是在實際調(diào)用發(fā)生時,是Derive::f()被調(diào)用了。這就實現(xiàn)了多態(tài)。

多重繼承(無虛函數(shù)覆蓋)

下面,再讓我們來看看多重繼承中的情況,假設(shè)有下面這樣一個類的繼承關(guān)系。注意:子類并沒有覆蓋父類的函數(shù)。

對于子類實例中的虛函數(shù)表,是下面這個樣子:

我們可以看到:

1) 每個父類都有自己的虛表。

2) 子類的成員函數(shù)被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調(diào)用到實際的函數(shù)。

多重繼承(有虛函數(shù)覆蓋)

下面我們再來看看,如果發(fā)生虛函數(shù)覆蓋的情況。

下圖中,我們在子類中覆蓋了父類的f()函數(shù)。

下面是對于子類實例中的虛函數(shù)表的圖:

我們可以看見,三個父類虛函數(shù)表中的f()的位置被替換成了子類的函數(shù)指針。這樣,我們就可以任一靜態(tài)類型的父類來指向子類,并調(diào)用子類的f()了。如:

Derive d;

Base1 *b1 = &d;

Base2 *b2 = &d;

Base3 *b3 = &d;

b1->f(); //Derive::f()

b2->f(); //Derive::f()

b3->f(); //Derive::f()

b1->g(); //Base1::g()

b2->g(); //Base2::g()

b3->g(); //Base3::g()

安全性

每次寫C++的文章,總免不了要批判一下C++。這篇文章也不例外。通過上面的講述,相信我們對虛函數(shù)表有一個比較細致的了解了。水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛函數(shù)表來干點什么壞事吧。

一、通過父類型的指針訪問子類自己的虛函數(shù)

我們知道,子類沒有重載父類的虛函數(shù)是一件毫無意義的事情。因為多態(tài)也是要基于函數(shù)重載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數(shù),但我們根本不可能使用下面的語句來調(diào)用子類的自有虛函數(shù):

Base1 *b1 = new Derive();

b1->f1(); //編譯出錯

任何妄圖使用父類指針想調(diào)用子類中的未覆蓋父類的成員函數(shù)的行為都會被編譯器視為非法,所以,這樣的程序根本無法編譯通過。但在運行時,我們可以通過指針的方式訪問虛函數(shù)表來達到違反C++語義的行為。(關(guān)于這方面的嘗試,通過閱讀后面附錄的代碼,相信你可以做到這一點)

二、訪問non-public的虛函數(shù)

另外,如果父類的虛函數(shù)是private或是protected的,但這些非public的虛函數(shù)同樣會存在于虛函數(shù)表中,所以,我們同樣可以使用訪問虛函數(shù)表的方式來訪問這些non-public的虛函數(shù),這是很容易做到的。

如:

class Base {

private:

virtual void f() { cout << "Base::f" << endl; }

};

class Derive : public Base{

};

typedef void(*Fun)(void);

void main() {

Derive d;

Fun pFun = (Fun)*((int*)*(int*)(&d)+0);

pFun();

}

結(jié)束語

C++這門語言是一門Magic的語言,對于程序員來說,我們似乎永遠摸不清楚這門語言背著我們在干了什么。需要熟悉這門語言,我們就必需要了解C++里面的那些東西,需要去了解C++中那些危險的東西。不然,這是一種搬起石頭砸自己腳的編程語言。


                                                                                                      [轉(zhuǎn)載內(nèi)容]

下面這個是對第一個例子的更進一步的說明
By zieckey (http://blog.chinaunix.net/u/16292/)

C++中的虛函數(shù)的作用主要是實現(xiàn)了多態(tài)的機制。關(guān)于多態(tài),簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調(diào)用實際子類的成員函數(shù)。這種技術(shù)可以讓父類的指針有“多種形態(tài)”,這是一種泛型技術(shù)。所謂泛型技術(shù),說白了就是試圖使用不變的代碼來實現(xiàn)可變的算法。比如:模板技術(shù),RTTI技術(shù),虛函數(shù)技術(shù),要么是試圖做到在編譯時決議,要么試圖做到運行時決議。

對C++ 了解的人都應(yīng)該知道虛函數(shù)(Virtual Function)是通過一張?zhí)摵瘮?shù)表(Virtual Table)來實現(xiàn)的。簡稱為V-Table。在這個表中,主是要一個類的虛函數(shù)的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應(yīng)實際的函數(shù)。這樣,在有虛函數(shù)的類的實例中這個表被分配在了這個實例的內(nèi)存中,所以,當我們用父類的指針來操作一個子類的時候,這張?zhí)摵瘮?shù)表就顯得由為重要了,它就像一個地圖一樣,指明了實際所應(yīng)該調(diào)用的函數(shù)。


這里我們著重看一下這張?zhí)摵瘮?shù)表。在C++的標準規(guī)格說明書中說到,編譯器必需要保證虛函數(shù)表的指針存在于對象實例中最前面的位置(這是為了保證正確取到虛函數(shù)的偏移量)。 這意味著我們通過對象實例的地址得到這張?zhí)摵瘮?shù)表,然后就可以遍歷其中函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)。

假設(shè)我們有這樣的一個類:

 

class Base {

     
public:

            
virtual void f() { cout << "Base::f" << endl; }

            
virtual void g() { cout << "Base::g" << endl; }

            
virtual void h() { cout << "Base::h" << endl; }

}

 


按照上面的說法,我們可以通過Base的實例來得到虛函數(shù)表。 下面是實際例程:



 

#include <stdio.h>

class Base {
public:
    
virtual void a() { printf("Base::a()\n"); }
    
virtual void b() { printf("Base::b()\n"); }
    
virtual void c() { printf("Base::c()\n"); }
    
virtual ~Base(){}
}


/*
定義一個無型參的返回類型為void的函數(shù)指針變量類型:Fun,
使用方法: 
Fun pFun;
pFun=function;//function為已經(jīng)定義的無型參的返回類型為void的函數(shù)
pFun();
*/

typedef 
void(*Fun)();

int main(int argc, char* argv[])
{
    Base 
*= new Base();
    printf(
"Base對象b的地址:%p\n", b );//b為Base類的一個實例對象的首地址,此地址開始的四字節(jié)的內(nèi)容存放的是虛函數(shù)表的地址

    
    printf(
"虛函數(shù)表地址:%p\n", (int*)( *(int*)b ) );
    
    printf(
"虛函數(shù)表第一個地址(該地址內(nèi)的 內(nèi)容為第一個函數(shù)的地址):%p\n", (int*)(*(int*)b) );//此處實際上就是虛函數(shù)表的首地址

    printf(
"虛函數(shù)表第二個地址(該地址內(nèi)的 內(nèi)容為第二個函數(shù)的地址):%p\n", (int*)(*(int*)b) +1 );
    printf(
"虛函數(shù)表第三個地址(該地址內(nèi)的 內(nèi)容為第三個函數(shù)的地址):%p\n", (int*)(*(int*)b) +2 );
    
    printf(
"虛函數(shù)表 ——第一個函數(shù)地址:%p\n", (int*)*((int*)(*(int*)b)) );
    printf(
"虛函數(shù)表 ——第二個函數(shù)地址:%p\n", (int*)*((int*)(*(int*)b) +1) );
    printf(
"虛函數(shù)表 ——第三個函數(shù)地址:%p\n", (int*)*((int*)(*(int*)b) +2) );

    Fun pFun 
= (Fun)*( (int*)(*(int*)b)+1 );
    pFun();
    ((Fun)
*( (int*)*(int*)b+2 )) ();
    
return 0;
}

 


運行結(jié)果:


 

Base對象b的地址:0x9281008
虛函數(shù)表地址:
0x80489c8
虛函數(shù)表第一個地址(該地址內(nèi)的 內(nèi)容為第一個函數(shù)的地址):
0x80489c8
虛函數(shù)表第二個地址(該地址內(nèi)的 內(nèi)容為第二個函數(shù)的地址):
0x80489cc
虛函數(shù)表第三個地址(該地址內(nèi)的 內(nèi)容為第三個函數(shù)的地址):
0x80489d0
虛函數(shù)表 ——第一個函數(shù)地址:
0x80486e8
虛函數(shù)表 ——第二個函數(shù)地址:
0x80486d4
虛函數(shù)表 ——第三個函數(shù)地址:
0x80486c0
Base::g()
Base::h()

 



下面來解釋一下程序中比較費解的表達式。

    a、printf("虛函數(shù)表地址:%p\n", (int*)( *(int*)b ) );
這一句,b是一個Base類型的指針,(int*)b把這個指針自身所在的內(nèi)存地址取出來了,*(int*)b把這個地址的內(nèi)容的一個4字節(jié)數(shù)據(jù)取出來了,這個4B數(shù)據(jù)本身又是一個地址,所以做了(int*)的強制轉(zhuǎn)換,就是(int*)( *(int*)b )了。
這里注意“*(int*)b” 與“*b”的不同,b是一個Base類型的指針,同時也是一個地址,那么 *b 就代表了一個Base類型的變量了,而“*(int*)b”卻只是把b這個地址的一個4字節(jié)數(shù)據(jù)取出來了。


    b、printf("虛函數(shù)表第二個地址(該地址內(nèi)的 內(nèi)容為第二個函數(shù)的地址):%p\n", (int*)(*(int*)b) +1   );
“(int*)(*(int*)b) +1”這個有上面的解釋可知是在“(int*)(*(int*)b) ”地址基礎(chǔ)上,增加4B偏移量,那么很自然的該地址的內(nèi)容就是第二個虛函數(shù)的首地址。


    c、Fun pFun = (Fun)*(  (int*)(*(int*)b)+1  );
前面typedef已經(jīng)處已經(jīng)給出了說明,    (Fun)*(  (int*)(*(int*)b)+1  )實際上是把地址 “*(  (int*)(*(int*)b)+1  )”強制性轉(zhuǎn)換為一個函數(shù)的入口地址,該函數(shù)無型參返回void。

同過這幾點的解釋,這個程序看懂應(yīng)該沒有問題了。
   
也許你不太相信程序運行的結(jié)果,沒關(guān)系,一開始我也不敢確定是否正確,這里我們可通過GDB調(diào)試看看內(nèi)存就知道了:



 

[root@localhost src]# g++ virtualTable.cpp -g
[root@localhost src]# gdb a.
out 
GNU gdb Red Hat Linux (
6.6-8.fc7rh)
Copyright (C) 
2006 Free Software Foundation, Inc.
GDB 
is free software, covered by the GNU General Public License, and you are
welcome to change it and
/or distribute copies of it under certain conditions.
Type 
"show copying" to see the conditions.
There 
is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured 
as "i386-redhat-linux-gnu"
Using host libthread_db library 
"/lib/libthread_db.so.1".
(gdb) b main
Breakpoint 
1 at 0x8048566: file virtualTable.cpp, line 22.
(gdb) r
Starting program: 
/mnt/study/unix/document/C_CPP_Programming/src/a.out 

Breakpoint 
1, main () at virtualTable.cpp:22
22 Base *= new Base();
(gdb) n
23 printf("Base對象b的地址:%p\n", b );//b為Base類的一個實例對象的首地址,此地址開始的四字節(jié)的內(nèi)容存放的是虛函數(shù)表的地址
(gdb) 
Base對象b的地址:
0x8f6c008
25 printf("虛函數(shù)表地址:%p\n", (int*)( *(int*)b) );
(gdb) 
虛函數(shù)表地址:
0x80489c8
27 printf("虛函數(shù)表第一個地址(該地址內(nèi)的 內(nèi)容為第一個函數(shù)的地址):%p\n", (int*)(*(int*)b) );//此處實際上就是虛函數(shù)表的首地址
(gdb) x/1aw 0x8f6c008    ======>這一命令打印出對象b的首地址‘0x8f6c008’的內(nèi)容,是虛函數(shù)表的地址,與上面由程序打印的虛函數(shù)表地址一致
0x8f6c0080x80489c8 <_ZTV4Base+8>
(gdb) n
虛函數(shù)表第一個地址(該地址內(nèi)的 內(nèi)容為第一個函數(shù)的地址):
0x80489c8
28 printf("虛函數(shù)表第二個地址(該地址內(nèi)的 內(nèi)容為第二個函數(shù)的地址):%p\n", (int*)(*(int*)b) +1 );
(gdb) n
虛函數(shù)表第二個地址(該地址內(nèi)的 內(nèi)容為第二個函數(shù)的地址):
0x80489cc
29 printf("虛函數(shù)表第三個地址(該地址內(nèi)的 內(nèi)容為第三個函數(shù)的地址):%p\n", (int*)(*(int*)b) +2 );
(gdb) n
虛函數(shù)表第三個地址(該地址內(nèi)的 內(nèi)容為第三個函數(shù)的地址):
0x80489d0
31 printf("虛函數(shù)表 ——第一個函數(shù)地址:%p\n", (int*)*((int*)(*(int*)b)) );
(gdb) x
/3aw 0x80489c8    ======>這一命令打印出虛函數(shù)首地址‘0x80489c8’開始的以4字節(jié)為單位的三個單位的內(nèi)存里的內(nèi)容,正好與下面三個虛函數(shù)的地址一致
0x80489c8 <_ZTV4Base+8>0x80486e8 <_ZN4Base1aEv> 0x80486d4 <_ZN4Base1bEv> 0x80486c0 <_ZN4Base1cEv>
(gdb) n
虛函數(shù)表 ——第一個函數(shù)地址:
0x80486e8
32 printf("虛函數(shù)表 ——第二個函數(shù)地址:%p\n", (int*)*((int*)(*(int*)b) +1) );
(gdb) 
虛函數(shù)表 ——第二個函數(shù)地址:
0x80486d4
33 printf("虛函數(shù)表 ——第三個函數(shù)地址:%p\n", (int*)*((int*)(*(int*)b) +2) );
(gdb) 
虛函數(shù)表 ——第三個函數(shù)地址:
0x80486c0
35 Fun pFun = (Fun)*( (int*)(*(int*)b)+1 );
(gdb) c
Continuing.
Base::b()    
======>這里的通過虛函數(shù)的地址,用函數(shù)指針的方式訪問虛函數(shù),得到的結(jié)果正常,表明上述虛函數(shù)地址沒有錯誤。
Base::c()

Program exited normally.
(gdb) 

 



參考文檔:
http://blog.csdn.net/haoel/archive/2007/12/18/1948051.aspx
http://blog.chinaunix.net/u/16292/showart_673980.html


補充說明:gdb命令
x/3aw 0x80489c8
表示從內(nèi)存地址 0x80489c8 讀取內(nèi)容,
w表示以四字節(jié)為一個單位,
3表示連續(xù)讀取三個單位,
a表示按十進制顯示
具體可以參考:    http://fanqiang.chinaunix.net/program/other/2005-03-23/2993.shtml