西西軟件下載最安全的下載網(wǎng)站、值得信賴(lài)的軟件下載站!

首頁(yè)編程開(kāi)發(fā)VC|VC++ → C++面向?qū)ο髾C(jī)制的實(shí)現(xiàn)

C++面向?qū)ο髾C(jī)制的實(shí)現(xiàn)

相關(guān)軟件相關(guān)文章發(fā)表評(píng)論 來(lái)源:西西整理時(shí)間:2013/2/3 20:59:09字體大。A-A+

作者:南樹(shù)點(diǎn)擊:0次評(píng)論:0次標(biāo)簽: 面向?qū)ο?/a>

我曾經(jīng)自學(xué)過(guò)C++,現(xiàn)在回想起來(lái),當(dāng)時(shí)是什么都不懂。說(shuō)不上能使用C++,倒是被C++牽著鼻子走了。高中搞NOIP并不允許使用STL庫(kù),比賽中C++面向?qū)ο蟮臋C(jī)制基本沒(méi)有什么用武之地,所以高中搞NOIP名為用C++,其實(shí)就是c加上了cout和cin。

前幾天看韓老師的《老碼識(shí)途》,里面記錄了一些C++面向?qū)ο髾C(jī)制的探索,又勾起了我的興趣。而這個(gè)學(xué)期自學(xué)了匯編,又給了我自己動(dòng)手探索提供了能力基礎(chǔ),自己上手以后,從一個(gè)更加底層的視角看C++機(jī)制的實(shí)現(xiàn),讓我在黑暗中摸到了馴服C++的韁繩。

本質(zhì)上是指針,這一點(diǎn)即使大家沒(méi)有看反匯編應(yīng)該也是猜到了。

對(duì)象在內(nèi)存上的布局

   1: class Father
   2: {
   3:     int iA_;
   4:     int iB_;
   5:     
   6:     void FuncA();
   7:     void FuncB();
   8: };
   9:  
  10: class Child : Father
  11: {
  12:     int iC_;
  13:     void FuncC();
  14: };

一個(gè)Father對(duì)象里只包含 (低地址 –> 高地址) : iA_,iB_。也就是一個(gè)Father對(duì)象的大小是8個(gè)字節(jié),函數(shù)并不會(huì)占用內(nèi)存空間。
為什么不會(huì)?

其實(shí)類(lèi)的成員函數(shù)可以看做本質(zhì)上與普通函數(shù)相同。

編譯器在編譯的時(shí)候就知道函數(shù)的位置,所以調(diào)用普通函數(shù)的時(shí)候會(huì)直接 call 函數(shù)地址(偏移)。也就是被硬編碼了,函數(shù)的地址是固定的( 不考慮重定位之類(lèi)的情況 )。

而成員函數(shù)的調(diào)用也是如此,只是編譯器還多做了一件事情,就是判斷這個(gè)對(duì)象有沒(méi)有調(diào)用這個(gè)函數(shù)的“權(quán)限”(函數(shù)不是你聲明的,當(dāng)然無(wú)權(quán)調(diào)用),“權(quán)限”不夠就會(huì)報(bào)錯(cuò),告訴那個(gè)對(duì)象類(lèi)型沒(méi)有這個(gè)方法。

所以,類(lèi)對(duì)象的大小與這個(gè)類(lèi)的方法數(shù)多少是沒(méi)關(guān)系的。成員函數(shù)和普通函數(shù)本質(zhì)上一樣,實(shí)現(xiàn)這個(gè)機(jī)制,要靠編譯器來(lái)做工作。

this指針:

成員函數(shù)與普通函數(shù)不同之處之一就是訪問(wèn)對(duì)象的數(shù)據(jù)。

要訪問(wèn)一個(gè)對(duì)象的元素,說(shuō)白了就是要找到這個(gè)元素所在的內(nèi)存位置,也就是要有指針。

我們沒(méi)有看到傳遞this指針,因?yàn)檫@件事又是編譯器幫我們做了。

反匯編會(huì)看到對(duì)象調(diào)用一個(gè)方法的時(shí)候,會(huì)將這個(gè)對(duì)象的首部地址賦值給ecx寄存器,通過(guò)寄存器來(lái)傳遞this指針。

我們?cè)诔蓡T函數(shù)里可以不需明寫(xiě)this指針地調(diào)用對(duì)象元素,還是因?yàn)榫幾g器幫我們多做了一步“翻譯”。

私有化:

不多說(shuō),就是編譯器在編譯階段通過(guò)源碼來(lái)判斷某個(gè)元素是不是能夠被訪問(wèn),某個(gè)方法是不是能夠被調(diào)用,運(yùn)行的時(shí)候并不會(huì)有訪問(wèn)限制?创a:

   1: #include <stdio.h>
   2:  
   3: class Exp
   4: {
   5:     int iA_;
   6:     int iB_;
   7:  
   8: public:
   9:     Exp()
  10:     {
  11:         iA_ = iB_ = 0;
  12:     }
  13:     void Out()
  14:     {
  15:         printf("%d \t %d \n",iA_,iB_);
  16:     }
  17: };
  18:  
  19: int main()
  20: {
  21:     Exp oA;
  22:     void *pC = &oA;
  23:  
  24:     oA.Out();
  25:     *(int*)pC = 1;
  26:     *(int*)((int)pC+4) = 2;
  27:     oA.Out();
  28:  
  29:     return 0;
  30: }

結(jié)果是: 0    0

             1    2

雖然 iA_,iB_是私有的,但是還是被外界修改了。因?yàn)榫幾g器無(wú)法知道我干了這事(顯式的 oA.iA_ = 1 就被發(fā)現(xiàn)了哈)

構(gòu)造與析構(gòu):

說(shuō)道底還是編譯器幫我們?cè)诙嘧隽艘恍┕ぷ,生成了一些額外代碼。

需要注意的是:

   1: void Test( Father oP )
   2: {
   3: }
   4:  
   5: int main()
   6: {
   7:     Father oA;
   8:     Test(oA);
   9:     return 0;
  10: }

會(huì)調(diào)用拷貝構(gòu)造函數(shù)。

重載:

一樣還是編譯器的功勞,C++最后生成的函數(shù)名是與參數(shù)有關(guān)的,所以又不同參數(shù)的函數(shù)最后生成的函數(shù)名不同,看似同名,實(shí)則不同。在函數(shù)調(diào)用的時(shí)候,編譯器會(huì)判斷參數(shù)的類(lèi)型,相應(yīng)的可以生成一個(gè)函數(shù)名進(jìn)行“匹配”。( 當(dāng)然不止這么簡(jiǎn)單,還會(huì)考慮發(fā)生類(lèi)型轉(zhuǎn)換的情況 )

繼承:

從內(nèi)存布局的角度上看

   1: struct Child : Father

   1: struct Child
   2: {
   3:     Father o;
   4:     //other
   5: };

相同(虛函數(shù)情況后面討論)。子類(lèi)的前面部分和父類(lèi)是一樣的。

所以一個(gè)接受 Father * 參數(shù)的函數(shù)可以接受 Child *參數(shù),而且轉(zhuǎn)換是安全的。

有 Father & 類(lèi)型參數(shù)的函數(shù)可以接受 Child &,但是繼承方式要public。But , why ?

protected和private繼承模式,子類(lèi)繼承的父類(lèi)的接口對(duì)外都是隱藏的,所以以一個(gè)Father &傳入的參數(shù)所有的方法元素原則上是不可用的,用了肯定是違反規(guī)則的,編譯器判定這一點(diǎn),所以報(bào)錯(cuò)。

虛函數(shù):

比較特別的是這個(gè)。

Question:為什么需要虛函數(shù)?

網(wǎng)上看到的答案:基類(lèi)可以通過(guò)虛函數(shù)對(duì)子類(lèi)的相識(shí)功能進(jìn)行管理。(我的C++primer被借走以后就此失蹤,所以只能網(wǎng)上找了)。

虛函數(shù)具體怎么回事就不細(xì)說(shuō)了,討論一下背后的機(jī)制。

為了能夠?qū)崿F(xiàn)虛函數(shù),每個(gè)有虛函數(shù)的類(lèi)有一張對(duì)應(yīng)的虛表。這個(gè)虛表儲(chǔ)存在只讀內(nèi)存區(qū),記錄了對(duì)應(yīng)函數(shù)的地址。(PS:一個(gè)類(lèi)就只有一個(gè)虛表)

每個(gè)類(lèi)對(duì)象都要保存一個(gè)虛表指針,保存本類(lèi)的虛表地址。所以你使用 Father *指針指向一個(gè)Child對(duì)象,調(diào)用的虛函數(shù)是Child的。

虛表指針保存在每個(gè)對(duì)象的首部。

   1: class Child : Father
   2: {
   3:     int iC_;
   4:     void FuncC();
   5:     virtual void VF();
   6: };

現(xiàn)在這個(gè)Child對(duì)象較前面的多了四個(gè)字節(jié)。內(nèi)存布局(從低地址到高地址)是:虛表指針__vfptr,iA_,iB_,iC_。

好。問(wèn)題來(lái)了,Child繼承了Father,但是Father的函數(shù)并沒(méi)有為Child再量身定做一次,也就是說(shuō)無(wú)論是Father對(duì)象還是Child對(duì)象,他們調(diào)用FuncA()都是同一個(gè)函數(shù)。但是Father并沒(méi)有__vfptr,Child對(duì)象在頭部多了這個(gè),F(xiàn)uncA()中用this指針定位iA_和iB_不是都不正確嗎?

現(xiàn)象告訴我們FuncA()是可以正確訪問(wèn)iA_和iB_,所以推測(cè)Child對(duì)象在調(diào)用FuncA的時(shí)候,傳的不是真正的首部地址,而是往后偏移了四個(gè)字節(jié)。

反匯編,確實(shí)如此。這么說(shuō)Father類(lèi)里不能調(diào)用虛函數(shù)了?當(dāng)然,F(xiàn)ather都還不知道虛函數(shù)這回事,怎么在FuncA中調(diào)用。

還有一個(gè)有趣的現(xiàn)象:

   1: #include <stdio.h>
   2:  
   3: class Base
   4: {
   5: public:
   6:     virtual void ShowID()
   7:     {
   8:         printf("Base\n");
   9:     }
  10: };
  11:  
  12: class CB : public Base
  13: {
  14: public:
  15:     virtual void ShowID()
  16:     {
  17:         printf("CB\n");
  18:     }
  19: };
  20:  
  21: class CC : public Base
  22: {
  23: public:
  24:     virtual void ShowID()
  25:     {
  26:         printf("CC\n");
  27:     }
  28: };
  29:  
  30: void Test( CB& oB )
  31: {
  32:     oB.ShowID();
  33: }
  34:  
  35: int main()
  36: {
  37:     Base oBase;
  38:     CB    oB;
  39:     CC    oC;
  40:  
  41:     CB* pCB = &oB;
  42:     
  43:     *(int*)(&oB) = *(int*)(&oC);    //修改虛表指針
  44:     oB.ShowID();
  45:     ((CB*)(&oB))->ShowID();
  46:     pCB->ShowID();
  47:     Test(oB);
  48:     
  49:     return 0;
  50: }

猜猜結(jié)果啊,買(mǎi)定離手。

結(jié)果是:CB   CB   CC    CC

在43行的地方,修改了oB的虛表指針,讓其指向CC類(lèi)的虛表。

但是oB.ShowID()沒(méi)理會(huì)我們的修改,還是調(diào)用CB類(lèi)的ShowID。反匯編,發(fā)現(xiàn)他沒(méi)走“獲取虛表指針,在虛表中得到相應(yīng)的函數(shù)地址”這一套,直接調(diào)用了。因?yàn)橐话闳瞬粫?huì)閑著蛋疼去改對(duì)象的虛表指針的,對(duì)象的類(lèi)型是明確的,編譯器可以通過(guò)這些信息確定調(diào)用的函數(shù)地址,所以沒(méi)必要走他一套,這樣效率還更高。

而pCB->ShowID()就不同了,他很乖地地走了流程,因?yàn)橐粋(gè)父類(lèi)指針可以指向一個(gè)子類(lèi)對(duì)象,編譯器無(wú)法找信息,所以走流程。

那現(xiàn)在糾結(jié)了,為神馬 ((CB*)(&oB))->ShowID() 輸出CB。

反匯編看,發(fā)現(xiàn)編譯器又擅自做主,沒(méi)有走指針的流程。

那你猜猜((Base*)(&oB))->ShowID();輸出的是什么?CC。

比較二者的差異,可以大概發(fā)現(xiàn)一些端倪,什么時(shí)候走流程,什么時(shí)候不走。

最后是Test(oB)了,前面說(shuō)過(guò)引用的本質(zhì)是指針,所以這個(gè)結(jié)果很好理解。

還有,想過(guò)

   1: void Test2( Base oP )
   2: {
   3:     oP.ShowID();
   4: }

拷貝的時(shí)候有沒(méi)有拷貝虛表指針嗎?試試就知道,厄…發(fā)現(xiàn)沒(méi)有。

前面說(shuō)過(guò)這樣會(huì)調(diào)用拷貝構(gòu)造函數(shù),但是你在這個(gè)函數(shù)你沒(méi)有寫(xiě)虛表指針的賦值。但是邪惡的編譯器已經(jīng)幫你悄悄加上去了哈哈哈哈~。(唉?節(jié)操呢)

RTTI

每個(gè)類(lèi)有特定的虛表地址,每個(gè)對(duì)象會(huì)保存這個(gè)虛表地址,應(yīng)該想到了吧,偷懶,不寫(xiě)了。

綜上。可以看到,面向?qū)ο髾C(jī)制在底層并不特別,機(jī)制的實(shí)現(xiàn)主要靠的是編譯器。

    推薦文章

    沒(méi)有數(shù)據(jù)

      沒(méi)有數(shù)據(jù)
    相關(guān)下載

    名稱(chēng)大小下載

    最新文章
      沒(méi)有數(shù)據(jù)