C++分析研究 |
|
|||
C++ |
日歷
統計
導航常用鏈接留言簿隨筆檔案
文章檔案搜索最新評論
閱讀排行榜評論排行榜 |
我曾經自學過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 每個類有特定的虛表地址,每個對象會保存這個虛表地址,應該想到了吧,偷懶,不寫了。 綜上。可以看到,面向對象機制在底層并不特別,機制的實現主要靠的是編譯器。
|
![]() |
|
Copyright © HAOSOLA | Powered by: 博客園 模板提供:滬江博客 |