99热99这里只有精品6国产,亚洲中文字幕在线天天更新,在线观看亚洲精品国产福利片 ,久久久久综合网

歡迎加入QQ討論群258996829
麥子學(xué)院 頭像
蘋果6袋
6
麥子學(xué)院

C++ 虛函數(shù)表及調(diào)用規(guī)范

發(fā)布時(shí)間:2016-10-17 23:44  回復(fù):0  查看:2790   最后回復(fù):2016-10-17 23:44  

在支付工具想做社交,即時(shí)通訊工具想做app市場(chǎng),英語字典想做新聞社交的今天,創(chuàng)造這些怪象的公司要求程序員懂得更多幾乎是理所當(dāng)然的,畢竟現(xiàn)在大家什么都想做。這不,正值招聘季,實(shí)驗(yàn)室的幾位學(xué)長也是一直在討論各種問題,發(fā)現(xiàn)對(duì)于C++語言而言,問的最多的還是虛函數(shù)表和STL

STL的考點(diǎn)至少是實(shí)用的,哪怕要求你讀過源碼,也并不過分,畢竟知根知底才能更好地應(yīng)用。但要求程序員掌握對(duì)象模型著實(shí)拎不清,因?yàn)檫@幾乎用不到,遠(yuǎn)沒有在設(shè)計(jì)模式上投入時(shí)間實(shí)在,或許它們最希望的是拿批發(fā)價(jià)招語言專家。。。

我已經(jīng)近2年沒用C++了,大三時(shí)下定覺心不再碰C++,因此原本這個(gè)博客里是不應(yīng)該出現(xiàn)任何C++相關(guān)的內(nèi)容的,然而讀研后,實(shí)驗(yàn)室項(xiàng)目就是C++寫的,只能拾起繼續(xù)用,唯一的區(qū)別只是我不再會(huì)花大把的時(shí)間鉆研其實(shí)現(xiàn)和語法規(guī)則了,人生苦短。

經(jīng)歷過n次考前臨陣磨槍后,我得到了一個(gè)結(jié)論:對(duì)待用不到的知識(shí),最好的方式是遺忘,區(qū)別只是讓它保留多久。

既然招聘的時(shí)候會(huì)考虛函數(shù)表,我又用不到,那就只能把它記錄下來,以便屆時(shí)快速記憶?,F(xiàn)在關(guān)于C++ vtab的文章早已爛大街,別太較真,筆記而已。

環(huán)境?

這里提及環(huán)境的原因有2點(diǎn):

調(diào)用規(guī)范(call convention)和環(huán)境相關(guān)

避嫌

本文的環(huán)境為Linux x86_64,編譯器為GCC 6.1。

虛函數(shù)表

對(duì)于面向?qū)ο蟮闹С郑沟?/span>C++的抽象能力相對(duì)于C有了長足的進(jìn)步,而抽象能力的改進(jìn)為復(fù)用(reuse)提供了有力支撐。封裝,繼承和多態(tài)3個(gè)特性中,最容易實(shí)現(xiàn)的是封裝,其次是繼承,涉及到了多重繼承這個(gè)大坑,它們都能夠在編譯時(shí)直接確定,但是多態(tài)則正好相反,根本無法在編譯時(shí)確定該調(diào)用哪個(gè)函數(shù),所以需要找到一種方法使得程序在運(yùn)行期間能夠正確定位函數(shù)。


想要實(shí)現(xiàn)這個(gè)特性,需要先明確不變這兩個(gè)關(guān)鍵要素:的是實(shí)例的類型,不變的是實(shí)例的地址。類型無法確定,且由于重寫(override)的引入,使得編譯器無法準(zhǔn)確從符號(hào)表中定位函數(shù)的地址。那么就只能從不變的部分入手了,由于地址是不變的,因此我們至少有如下幾種方式來實(shí)現(xiàn):

runtime維護(hù)一個(gè)表,地址作為key,定位類型或者函數(shù)列表

實(shí)例頭部持有一個(gè)標(biāo)識(shí),使其能夠定位成員函數(shù)列表

實(shí)例持有指向?qū)?yīng)函數(shù)列表的指針

實(shí)例直接持有函數(shù)列表

方案1使得classstruct的存儲(chǔ)模型幾乎一致,但是函數(shù)調(diào)用的開銷會(huì)大一些,方案2和方案3本質(zhì)上是一樣的,方案4占用空間大,C++采用的是方案3。原則上應(yīng)該避免讓runtime持有數(shù)據(jù),因?yàn)檫@樣會(huì)在使用動(dòng)態(tài)鏈接庫時(shí)出現(xiàn)麻煩,暫時(shí)沒有深究C++是怎么解決這個(gè)問題的,簡單來看,多個(gè)runtime由于地址空間不重合,函數(shù)列表的地址必然不同,故而只需保證函數(shù)列表的內(nèi)容一致即可,假如有不同的版本,那么結(jié)果可能會(huì)讓人非常苦惱。

書歸正傳,C++實(shí)例持有的虛指針稱為VPTR,指向的虛函數(shù)表稱為VTAB,它是函數(shù)地址列表。

對(duì)于如下單繼承的情況,測(cè)試代碼如下:

class A {public:

    virtual void f1() { cout << "A::f1" << endl; }

    virtual void f2() { cout << "A::f2" << endl; }

};

class B : public A {public:

    void f1() { cout << "B::f1" << endl; }

};

typedef void (*Func)();int main() {

    B tt;

    Func* vptr = *(Func**)&tt;

    vptr[0]();

    vptr[1]();

    return 0;

}

該段用例反饋的存儲(chǔ)模型大致如下圖:

C++&nbsp;虛函數(shù)表及調(diào)用規(guī)范

可見,B類實(shí)例的頭部包含了一個(gè)指向VTAB的指針,接下來就可以依靠基址+偏移的方式進(jìn)行選擇調(diào)用了。

對(duì)于如下多重繼承的情況,測(cè)試代碼如下:

class B1 {public:

    virtual void fooB1() { cout << "B1::foo" << endl; }

    virtual void barB1() { cout << "B1::bar" << endl; }

};

class B2 {public:

    virtual void fooB2() { cout << "B2::foo" << endl; }

    virtual void barB2() { cout << "B2::bar" << endl; }

};

class D : public B1, B2 {public:

    void fooB1() { cout << "D::foo" << endl; }

    void barB2() { cout << "D::bar" << endl; }

};

typedef void (*Func)();int main() {

    D tt;

    Func* vptr1 = *(Func**)&tt;

    Func* vptr2 = *((Func**)&tt + 1);

    vptr1[0]();

    vptr1[1]();

    vptr2[0]();

    vptr2[1]();

    return 0;

}

該段用例反饋的存儲(chǔ)模型大致如下圖:

C++&nbsp;虛函數(shù)表及調(diào)用規(guī)范

和單繼承的情況差不多,只不過包含多個(gè)VPTR罷了,VPTR的數(shù)量和繼承的類型數(shù)量一致。

至于包含virtual繼承的多重繼承的情況,暫未涉及,因?yàn)槠綍r(shí)很少用,留作之后補(bǔ)充。

調(diào)用規(guī)范

事實(shí)上并不存在絕對(duì)的標(biāo)準(zhǔn),這也是為何為 調(diào)用規(guī)范 稱為 Call Convention 而不是Call Standard 的原因。常見的調(diào)用規(guī)范有:

cdecl

stdcall

fastcall

thiscall

通常它們都是可選的,我們總有方式告知編譯器該怎么做,這里我并不準(zhǔn)備詳細(xì)介紹它們的區(qū)別,因?yàn)?/span> wiki 寫的相當(dāng)詳盡。

C的調(diào)用規(guī)范是十分簡潔的,GCC默認(rèn)為cdecl,在x86上通過壓棧來傳參,x86_64估計(jì)是因?yàn)榧拇嫫鲾?shù)量多了,因此允許通過寄存器傳遞6個(gè)整型參數(shù)及8個(gè)浮點(diǎn)型參數(shù),其余繼續(xù)通過壓棧解決,但這通常足以覆蓋絕大多數(shù)情況。

那么,C++的調(diào)用規(guī)范呢?在給出答案前,需要先回答一個(gè)問題,以便更好地理解,C++相比C多了什么?

我的答案是更多的抽象層級(jí),命名空間、嵌套的命名空間、類空間、成員函數(shù)等等,它們都是新出現(xiàn)的抽象層級(jí),于是編譯器在符號(hào)表中尋找符號(hào)時(shí)就不可避免的會(huì)出現(xiàn)優(yōu)先級(jí)這個(gè)概念。所幸,絕大部分都可以通過優(yōu)先級(jí)來解決,唯獨(dú)成員函數(shù)有其特殊性。

成員函數(shù)不同于類函數(shù)(過度使用的static),后者是屬于類本身的,能夠訪問類空間的靜態(tài)變量和函數(shù),以及類所屬的外層命名空間的變量及函數(shù),類函數(shù)其實(shí)和C的函數(shù)沒什么區(qū)別,無需驚訝,事實(shí)就是如此,因此其調(diào)用規(guī)范使用cdecl即可。

問題在于成員函數(shù)是屬于類實(shí)例的,確切地說只能通過類實(shí)例來完成對(duì)成員函數(shù)的調(diào)用,原因在于成員變量因?qū)嵗悾梢曰叵胍幌?/span>this指針,this作為一個(gè)隱式參數(shù)進(jìn)行傳遞,沒有它便訪問不了成員變量了。

在這種情況下就會(huì)使用thiscall,它有點(diǎn)特殊,因?yàn)樗鼪]有明確的定義,既可以是stdcall加強(qiáng)版,通過rcx傳遞this指針,也可以是cdecl,通過將this作為第一個(gè)參數(shù)。

GCCthiscall基本就是cdecl,即this指針作為第一個(gè)參數(shù)。下面我們通過實(shí)例驗(yàn)證:

class Test {

    int num = 999;public:

    void dis(int n) { cout << this->num << n << endl; }

};

 

typedef void (*OFunc)(void*, int);

int main() {

    void (Test::* fdis) (int) = &Test::dis;

    OFunc of = (OFunc)fdis;

    Test test;

    Test* pt = &test;

    of(pt, 0);

    return 0;

}

雖然編譯器向我們抱怨了,但這并不影響最終結(jié)果,可以發(fā)現(xiàn)最終成員函數(shù)dis的類型被強(qiáng)制轉(zhuǎn)型后也能夠工作,這符合預(yù)期。

其實(shí),所謂的類型不過是供編譯器進(jìn)行類型檢查罷了,之后的過程就只有符號(hào)這個(gè)概念,而不會(huì)出現(xiàn)類型了,這也正是C++的符號(hào)如此惡心的原因之一,函數(shù)重載和干凈的符號(hào),C++選擇了前者。

寫在最后

知道了上述內(nèi)容后,我們獲得了什么?更加熟悉了C++的虛函數(shù)表(廢話),然后呢?有些知識(shí),了解后真的就只代表你知道,但你很可能永遠(yuǎn)都用不到,除非去設(shè)計(jì)程序語言的對(duì)象模型。

相反,這只會(huì)增加開發(fā)者使用奇技淫巧的可能性,對(duì)于尋常軟件而言,使用奇技淫巧并不是一件好事情,而杜絕這類狀況發(fā)生的最好方式應(yīng)該就是徹底屏蔽細(xì)節(jié)。在任何環(huán)境下,都不要試圖使用上文中的技巧,因?yàn)槟愫芸赡軙?huì)得到一個(gè)無法移植難以調(diào)試的程序。

 

文章來源:擼代碼

您還未登錄,請(qǐng)先登錄

熱門帖子

最新帖子

?