C 中 虛擬函式及包含多型的實現

NO IMAGE

我們分三個方面來說明虛擬函式以及用虛擬函式實現的包含多型。

第一個:什麼是虛擬函式?

從語法上來說虛擬函式就是用virtual
宣告的函式。所以定義一個虛擬函式很簡單。重點是你需要知道我們如何用虛擬函式解決實際的問題。

第二個:編譯器是如何解析函式呼叫語句的?

通常我們是用一個型別定義一個物件,或者new一個物件,然後用這個型別的指標指向它,然後用物件或者指標來呼叫它所擁有的函式。某些時候(其實是經常)我們會遇到在子類中覆蓋父類方法的情況,根據我們前面所說的指標賦值相容性規則,我們用下面的例子詳細說明一下這種語法情況,然後和後面的多型進行對比:

class A

{

public:

voidfun()

{

cout << “A的hello” << endl;

}

};

 

 

class B : public A

{

public:

voidfun()

{

cout << “B的hello” << endl;

}

};

 

int _tmain(int argc,_TCHAR* argv[])

{

//
現在主要說明賦值相容性規則

Aa;

a.fun(); // A的hello

Bb;

b.fun(); // B的hello 

//
以上程式碼不會有任何問題或歧義

 

Bb1;

b1.fun(); // B的hello 

A a1 = b1; //用子類物件給父類物件賦值

a1.fun(); // A的hello

 

Bb2;

b2.fun(); // B的hello 

 

A &a2 = b2; //
用子類物件給一個父類物件的引用賦值

a2.fun(); // A的hello

 

Bb3;

b3.fun(); // B的hello 

A *pa3 = &b3; //用子類物件地址給父類物件的指標賦值

pa3->fun(); // A的hello

 

B*pb4 = new B();

pb4->fun(); // B的hello

A *pa4 = pb4;//用子類的指標給父類的指標賦值。

pa4->fun();// A的hello

 

return0;

}

仔細觀察一個,編譯器在編譯一個方法呼叫語句時,總是根據呼叫方法的物件或者指標的型別來呼叫對應的方法。那麼就引出了我們的第三個問題。

第三:我們如何根據父類的指標來呼叫子類的方法呢?

答案就是我們前面所說的虛擬函式。

下面我們用實際的程式碼來看一下虛擬函式的作用,以及其記憶體模型。

class A

{

public:

virtualvoid fun()

{

cout << “A的hello” << endl;

}

};

 

 

class B : public A

{

public:

virtualvoid fun()

{

cout << “B的hello” << endl;

}

};

 

int _tmain(int argc,_TCHAR* argv[])

{

//
現在主要說明賦值相容性規則

Aa;

a.fun(); // A的hello

Bb;

b.fun(); // B的hello 

//
以上程式碼不會有任何問題或歧義

 

Bb1;

b1.fun(); // B的hello 

A a1 = b1; //

a1.fun(); // A的hello

 

Bb2;

b2.fun(); // B的hello 

 

A&a2 = b2;

a2.fun();
// Bhello

 

Bb3;

b3.fun(); // B的hello 

A*pa3 = &b3;

pa3->fun();
// Bhello

 

B*pb4 = new B();

pb4->fun(); // B的hello

A*pa4 = pb4;

pa4->fun();// Bhello

 

return0;

}

第一種情況,用B的物件給A的物件賦值,實際上是呼叫了A的拷貝建構函式,也就是用b1中A的部分去給a1賦值,所以我們可以認為B的物件中包含一個A物件(可能這就是為什麼叫做包含多型吧)。此時用a1呼叫fun()就確實是用A的物件來呼叫fun()方法

其他三種情況a2,a3,a4實際上表示或者指向的都是一個B型別的物件,所有後面輸出的就都是“B的hello”

 

為了弄清楚程式究竟是如何工作的,我們從反彙編和記憶體的角度來看一個它的記憶體模型。

在前面所說的物件的記憶體模型中,我們知道程式在執行時會先把方法程式碼載入到程式碼區,然後把方法的入口地址以jmp
地址
的方式給出方法入口表。而我們在程式碼中對方法的呼叫會被編譯器自動轉換為call
入口表中對應入口地址

的方式。

 

現在再來看一下含有虛擬函式的情況:

pb4->fun(); // B的hello

00FE6A91  mov         eax,dword ptr [pb4] 
//1- 把物件的首地址複製到eax

00FE6A94  mov         edx,dword ptr [eax] 
// 2-把以eax的內容為地址的4個位元組的記憶體空間的內容複製到edx,實際上它就是虛指標。

00FE6A96  mov        esi,esp 

00FE6A98  mov         ecx,dword ptr [pb4] 
 // ecx存放對應物件的首地址

00FE6A9B  mov         eax,dword ptr [edx] 
// 3-然後把以edx的內容為地址的記憶體空間的內容複製到eax,現在eax就是函式入口表中對應的函式入口地址了。

00FE6A9D  call        eax //4-呼叫,跳轉

00FE6A9F  cmp        esi,esp 

00FE6AA1  call       __RTC_CheckEsp (0FE136Bh) 

A *pa4 = pb4;

00FE6AA6  mov        eax,dword ptr [pb4] 

00FE6AA9  mov        dword ptr [pa4],eax 

pa4->fun();// B的hello

00FE6AAC  mov        eax,dword ptr [pa4] 

00FE6AAF  mov        edx,dword ptr [eax] 

00FE6AB1  mov        esi,esp 

00FE6AB3  mov        ecx,dword ptr [pa4] 

00FE6AB6  mov        eax,dword ptr [edx] 

00FE6AB8  call       eax 

如果你看完上面加粗的說明之後還沒有頭暈,那麼恭喜你,你不是正常人^^

作為正常人,我需要去看看記憶體中的具體資料。

第一步:mov  eax, dword ptr [pb4]      [pb4] == pb4 =
0x0061D7E0,執行之後eax =0x0061D7E0

它所對應的前四個位元組的內容為0x00FED998

第二步:mov edx,dword ptr [eax] ; [eax] =
以0x0061D7E0為地址的記憶體單元的內容,也就是0x00FED998

也就是虛指標,此時edx = 0x00FED998

我們看一下0x00FED998地址處的內容:

可以看到,此處的內容都是00fe開頭的一些記憶體空間的地址。

 

第三步:moveax,dword ptr [edx]; [edx] =
以0x00FED998
為首地址的記憶體單元的內容,也就是0x00FE151E

此時,EAX = 00FE151E

 

第四步:call eax;
在call後面,說明eax儲存的是可執行程式碼了,所以我們檢視一下此處的反彙編

A::A:

00FE1519  jmp        A::A (0FE68F0h) 

B::fun:

00FE151E jmp         B::fun (0FE2DD0h) 

A::fun:

00FE1523  jmp        A::fun (0FE2D70h) 

B::B:

00FE1528  jmp        B::B (0FE98F0h) 

A::A:

00FE152D  jmp        A::A (0FE6950h) 

可以看到,從call開始,進入到普通函式呼叫的函式入口表中的函式入口地址。

(ps:虛表和函式入口表是在一段記憶體空間中存放)

 

總結一下,對於含有虛擬函式的類,在生成物件時,會在物件的前四個位元組儲存一個虛指標,我們稱虛指標指向的記憶體空間是一個虛表,它是由一系列虛擬函式的入口的地址組成的,然後用call
進行程式的跳轉,回到普通函式的呼叫方法上。

 

對應的我們可以和虛基類對比一下,虛繼承之後的類在生成物件時,同樣會在物件開始儲存一個虛指標,只不過虛指標指向的虛表中儲存的不是函式的入口地址,而是物件中,基類的屬性成員的偏移地址。

 

現在我們已經說明了虛擬函式用途和使用方式,以及包含多型的意義,需要明確以下幾點:

1、虛擬函式會造成額外的記憶體開支,所以只應該在類層次結構中並且需要使用多型時才使用

2、虛擬函式應該是公有的,並且類層次之間的繼承也應該是公有的,否則就沒什麼意義了,

或者有其他的特殊情況,再特殊處理。