peakflys原創(chuàng)作品,轉(zhuǎn)載請(qǐng)注明源作者和源鏈接!
virtual function是很多公司面試題的重點(diǎn)考察內(nèi)容,雖然對(duì)于C++而言這是一個(gè)老生常談的話題了,但是工作中我發(fā)現(xiàn)還是有很多人理解的不透徹。
先看下面的一個(gè)例子:
/**
*\brief virtual function test case
*\author peakflys
*\date Sun Dec 1 14:52:47 CST 2013
*/
#include <iostream>
using namespace std;
class Base
{
public:
virtual void print(const int a = 10) {cout<<"Base: "<<a<<endl;}
};
class Derive : public Base
{
public:
virtual void print(const int a = 100) {cout<<"Derive: "<<a<<endl;}
};
int main()
{
Base *pb = new Derive;
pb->print();
Base& rb = *pb;
rb.print();
Derive d;
d.print();
Base *pbb = &d;
pbb->print();
Base& rbb = d;
rbb.print();
Base b;
b.print();
Derive *pd = (Derive*)&b;
pd->print();
Derive& rd = *(Derive*)&b;
rd.print();
delete pb;
return 0;
}
你認(rèn)為運(yùn)行后的結(jié)果是什么呢?
下面是在我機(jī)器上的運(yùn)行結(jié)果(Linux dev 2.6.32,gcc (GCC) 4.8.1)
Derive: 10
Derive: 10
Derive: 100
Derive: 10
Derive: 10
Base: 10
Base: 100
Base: 100
上面例子主要考察的內(nèi)容有四塊:虛函數(shù)的執(zhí)行、引用和指針的關(guān)系、函數(shù)調(diào)用過(guò)程、類(lèi)型強(qiáng)轉(zhuǎn)后的行為。如果你能答對(duì)所有的結(jié)果,下面的內(nèi)容可以略過(guò)。
下面我們來(lái)一一回顧一下所涉及到的這四塊內(nèi)容。
1、虛函數(shù)的運(yùn)行機(jī)理:
虛函數(shù)是C++實(shí)現(xiàn)多態(tài)性的必要手段,它在運(yùn)行時(shí)刻才決定具體該調(diào)用哪個(gè)函數(shù)。對(duì)于虛函數(shù)的完整細(xì)節(jié)實(shí)現(xiàn)標(biāo)準(zhǔn)并未給出,但是大多數(shù)編譯器廠商,包括GCC、VS的常見(jiàn)實(shí)現(xiàn)都是在含有虛函數(shù)的類(lèi)對(duì)象起始地址增加一個(gè)虛表指針,虛表指針指向的數(shù)組空間稱(chēng)之為虛表,這個(gè)數(shù)組包含了類(lèi)對(duì)象的所有虛函數(shù)地址。詳細(xì)內(nèi)容大家可以參看《Inside The C++ Object Model》的Function語(yǔ)義學(xué)(注:這本書(shū)里有部分結(jié)論和例子運(yùn)行同現(xiàn)在主流編譯器的實(shí)現(xiàn)有出入)。
2、引用的行為
在常見(jiàn)的編譯器中,引用一般都是通過(guò)指針來(lái)實(shí)現(xiàn)的,它同指針的區(qū)別就是它比指針有更多的約束,使用上有更多的限制。
3、虛函數(shù)的調(diào)用過(guò)程:
虛函數(shù)的調(diào)用過(guò)程通常是以下三個(gè)步驟:
①、參數(shù)壓棧
②、從虛表指針指向的虛表中找出函數(shù)的地址
③、調(diào)用函數(shù)。
這些操作都是在編譯時(shí)期就確定的,所不同的是運(yùn)行時(shí)刻對(duì)象不同,其對(duì)應(yīng)的虛表中函數(shù)地址自然也就是運(yùn)行時(shí)真實(shí)對(duì)象的函數(shù),這也就是虛函數(shù)實(shí)現(xiàn)的本質(zhì)。
而這個(gè)過(guò)程中,參數(shù)的入棧是對(duì)象無(wú)關(guān)的,而且是在編譯時(shí)期就確定下來(lái)的。所以上面例子中所有指針和引用所調(diào)用函數(shù)的參數(shù),都是指針和引用本身類(lèi)型對(duì)應(yīng)的函數(shù)默認(rèn)參數(shù),同運(yùn)行時(shí)刻他們真實(shí)指向的對(duì)象內(nèi)存無(wú)關(guān)。
4、類(lèi)型強(qiáng)轉(zhuǎn)后的行為
通常的類(lèi)型強(qiáng)轉(zhuǎn)是告訴編譯器必須按照指定結(jié)構(gòu)的內(nèi)存布局來(lái)解析對(duì)應(yīng)內(nèi)存,正如上例中”Derive *pd = (Derive*)&b; “ ,編譯器就會(huì)把b對(duì)應(yīng)的內(nèi)存來(lái)當(dāng)做Derive的內(nèi)存布局來(lái)解析,但是內(nèi)存里的內(nèi)容不變,所以虛函數(shù)運(yùn)行正常。
注:這種行為很危險(xiǎn),如果使用的內(nèi)存布局并不適合真實(shí)內(nèi)存,很可能造成訪問(wèn)越界等問(wèn)題,所以要格外小心強(qiáng)轉(zhuǎn)操作的使用!對(duì)于例子中的downcasting行為,建議使用C++提供的dynamic_cast來(lái)轉(zhuǎn)換。
為了大家更好的理解上面的內(nèi)容,特附上使用指針和引用分別調(diào)用虛函數(shù)過(guò)程的gcc匯編代碼和注釋?zhuān)?br />
Base *pb = new Derive;
400b49: bf 08 00 00 00 mov $0x8,%edi
400b4e: e8 6d fe ff ff callq 4009c0 <_Znwm@plt>
400b53: 48 89 c3 mov %rax,%rbx
400b56: 48 89 df mov %rbx,%rdi
400b59: e8 f4 01 00 00 callq 400d52 <_ZN6DeriveC1Ev> //以上均為Derive對(duì)象的構(gòu)造
400b5e: 48 89 5d e8 mov %rbx,-0x18(%rbp) //pb指針的賦值
pb->print();
400b62: 48 8b 45 e8 mov -0x18(%rbp),%rax //pb指針指向的內(nèi)存的首地址,即Derive對(duì)象的起始地址,亦即虛表指針的地址
400b66: 48 8b 00 mov (%rax),%rax //取虛表地址
400b69: 48 8b 00 mov (%rax),%rax //取虛表中的第一項(xiàng)內(nèi)容(因Derive和Base只有一個(gè)虛函數(shù)),即print函數(shù)地址
400b6c: 48 8b 55 e8 mov -0x18(%rbp),%rdx //this指針傳入rdx
400b70: be 0a 00 00 00 mov $0xa,%esi //參數(shù)10入棧(可見(jiàn)在編譯時(shí)期就已經(jīng)確定了)
400b75: 48 89 d7 mov %rdx,%rdi //this指針借rdx傳給rdi
400b78: ff d0 callq *%rax //調(diào)用虛函數(shù)(通過(guò)真實(shí)對(duì)象的虛表來(lái)確定的真正被調(diào)函數(shù))
Base& rb = *pb;
400b7a: 48 8b 45 e8 mov -0x18(%rbp),%rax
400b7e: 48 89 45 e0 mov %rax,-0x20(%rbp)
rb.print();
400b82: 48 8b 45 e0 mov -0x20(%rbp),%rax
400b86: 48 8b 00 mov (%rax),%rax
400b89: 48 8b 00 mov (%rax),%rax
400b8c: 48 8b 55 e0 mov -0x20(%rbp),%rdx
400b90: be 0a 00 00 00 mov $0xa,%esi
400b95: 48 89 d7 mov %rdx,%rdi
400b98: ff d0 callq *%rax //以上為通過(guò)引用調(diào)用虛函數(shù)的過(guò)程,可見(jiàn)同指針調(diào)用的實(shí)現(xiàn)完全相同,注釋略
通過(guò)上面的分析,相信大家應(yīng)該都能輕松的明白上面例子的運(yùn)行結(jié)果,此處不再一一解讀。
--by peakflys 15:57:06 Sunday, December 01, 2013