探秘C++機制的實現

來源:互聯網
上載者:User

    我曾經自學過C++,現在回想起來,當時是什麼都不懂。說不上能使用C++,倒是被C++牽著鼻子走了。高中搞NOIP並不允許使用STL庫,比賽中C++物件導向的機制基本沒有什麼用武之地,所以高中搞NOIP名為用C++,其實就是c加上了cout和cin。

    前幾天看韓老師的《老碼識途》,裡面記錄了一些C++物件導向機制的探索,又勾起了我的興趣。而這個學期自學了彙編,又給了我自己動手探索提供了能力基礎,自己上手以後,從一個更加底層的視角看C++機制的實現,讓我在黑暗中摸到了馴服C++的韁繩。

引用:

本質上是指標,這一點即使大家沒有看反組譯碼應該也是猜到了。

 

對象在記憶體上的布局:
   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: };

一個Father對象裡只包含 (低地址 –> 高地址) : iA_,iB_。也就是一個Father對象的大小是8個位元組,函數並不會佔用記憶體空間。
為什麼不會?其實類的成員函數可以看做本質上與普通函數相同。編譯器在編譯的時候就知道函數的位置,所以調用普通函數的時候會直接 call 函數地址(位移)。也就是被寫入程式碼了,函數的地址是固定的( 不考慮重定位之類的情況 )。而成員函數的調用也是如此,只是編譯器還多做了一件事情,就是判斷這個對象有沒有調用這個函數的“許可權”(函數不是你聲明的,當然無權調用),“許可權”不夠就會報錯,告訴那個物件類型沒有這個方法。所以,類對象的大小與這個類的方法數多少是沒關係的。成員函數和普通函數本質上一樣,實現這個機制,要靠編譯器來做工作。 this指標:成員函數與普通函數不同之處之一就是訪問對象的資料。要訪問一個對象的元素,說白了就是要找到這個元素所在的記憶體位置,也就是要有指標。我們沒有看到傳遞this指標,因為這件事又是編譯器幫我們做了。反組譯碼會看到對象調用一個方法的時候,會將這個對象的首部地址賦值給ecx寄存器,通過寄存器來傳遞this指標。我們在成員函數裡可以不需明寫this指標地調用對象元素,還是因為編譯器幫我們多做了一步“翻譯”。 私人化:不多說,就是編譯器在編譯階段通過源碼來判斷某個元素是不是能夠被訪問,某個方法是不是能夠被調用,啟動並執行時候並不會有訪問限制。看代碼:

   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: }

結果是: 0    0

             1    2

雖然 iA_,iB_是私人的,但是還是被外界修改了。因為編譯器無法知道我幹了這事(顯式的 oA.iA_ = 1 就被發現了哈)

 

構造與析構:

說道底還是編譯器幫我們在多做了一些工作,產生了一些額外代碼。

需要注意的是:

   1: void Test( Father oP )

   2: {

   3: }

   4:  

   5: int main()

   6: {

   7:     Father oA;

   8:     Test(oA);

   9:     return 0;

  10: }

 

會調用拷貝建構函式。

 

重載:

一樣還是編譯器的功勞,C++最後產生的函數名是與參數有關的,所以又不同參數的函數最後產生的函數名不同,看似同名,實則不同。在函數調用的時候,編譯器會判斷參數的類型,相應的可以產生一個函數名進行“匹配”。( 當然不止這麼簡單,還會考慮發生類型轉換的情況 )

 

繼承:

從記憶體布局的角度上看

   1: struct Child : Father

   1: struct Child

   2: {

   3:     Father o;

   4:     //other

   5: };

 

相同(虛函數情況後面討論)。子類的前面部分和父類是一樣的。

所以一個接受 Father * 參數的函數可以接受 Child *參數,而且轉換是安全的。

有 Father & 型別參數的函數可以接受 Child &,但是繼承方式要public。But , why ?

protected和private繼承模式,子類繼承的父類的介面對外都是隱藏的,所以以一個Father &傳入的參數所有的方法元素原則上是停用,用了肯定是違反規則的,編譯器判定這一點,所以報錯。

 

虛函數:

比較特別的是這個。

Question:為什麼需要虛函數?

網上看到的答案:基類可以通過虛函數對子類的相識功能進行管理。(我的C++primer被借走以後就此失蹤,所以只能網上找了)。

虛函數具體怎麼回事就不細說了,討論一下背後的機制。

為了能夠實現虛函數,每個有虛函數的類有一張對應的虛表。這個虛表儲存在唯讀記憶體區,記錄了對應函數的地址。(PS:一個類就只有一個虛表)

每個類對象都要儲存一個虛表指標,儲存本類的虛表地址。所以你使用 Father *指標指向一個Child對象,調用的虛函數是Child的。

虛表指標儲存在每個對象的首部。

   1: class Child : Father

   2: {

   3:     int iC_;

   4:     void FuncC();

   5:     virtual void VF();

   6: };

現在這個Child對象較前面的多了四個位元組。記憶體布局(從低地址到高地址)是:虛表指標__vfptr,iA_,iB_,iC_。

好。問題來了,Child繼承了Father,但是Father的函數並沒有為Child再量身定做一次,也就是說無論是Father對象還是Child對象,他們調用FuncA()都是同一個函數。但是Father並沒有__vfptr,Child對象在頭部多了這個,FuncA()中用this指標定位iA_和iB_不是都不正確嗎?

現象告訴我們FuncA()是可以正確訪問iA_和iB_,所以推測Child對象在調用FuncA的時候,傳的不是真正的首部地址,而是往後位移了四個位元組。

反組譯碼,確實如此。這麼說Father類裡不能調用虛函數了?當然,Father都還不知道虛函數這回事,怎麼在FuncA中調用。

還有一個有趣的現象:

   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: }

猜猜結果啊,買定離手。

結果是:CB   CB   CC    CC

在43行的地方,修改了oB的虛表指標,讓其指向CC類的虛表。

但是oB.ShowID()沒理會我們的修改,還是調用CB類的ShowID。反組譯碼,發現他沒走“擷取虛表指標,在虛表中得到相應的函數地址”這一套,直接調用了。因為一般人不會閑著蛋疼去改對象的虛表指標的,對象的類型是明確的,編譯器可以通過這些資訊確定調用的函數地址,所以沒必要走他一套,這樣效率還更高。

而pCB->ShowID()就不同了,他很乖地地走了流程,因為一個父類指標可以指向一個子類對象,編譯器無法找資訊,所以走流程。

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

反組譯碼看,發現編譯器又擅自做主,沒有走指標的流程。

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

比較二者的差異,可以大概發現一些端倪,什麼時候走流程,什麼時候不走。

最後是Test(oB)了,前面說過引用的本質是指標,所以這個結果很好理解。

還有,想過

   1: void Test2( Base oP )

   2: {

   3:     oP.ShowID();

   4: }

拷貝的時候有沒有拷貝虛表指標嗎?試試就知道,厄…發現沒有。

前面說過這樣會調用拷貝建構函式,但是你在這個函數你沒有寫虛表指標的賦值。但是邪惡的編譯器已經幫你悄悄加上去了哈哈哈哈~。(唉?節操呢)

 

 

RTTI

每個類有特定的虛表地址,每個對象會儲存這個虛表地址,應該想到了吧,偷懶,不寫了。

 

綜上。可以看到,物件導向機制在底層並不特別,機制的實現主要靠的是編譯器。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.