我曾經(jīng)自學過C++,現(xiàn)在回想起來,當時是什么都不懂。說不上能使用C++,倒是被C++牽著鼻子走了。高中搞NOIP并不允許使用STL庫,比賽中C++面向?qū)ο蟮臋C制基本沒有什么用武之地,所以高中搞NOIP名為用C++,其實就是c加上了cout和cin。
前幾天看韓老師的《老碼識途》,里面記錄了一些C++面向?qū)ο髾C制的探索,又勾起了我的興趣。而這個學期自學了匯編,又給了我自己動手探索提供了能力基礎(chǔ),自己上手以后,從一個更加底層的視角看C++機制的實現(xiàn),讓我在黑暗中摸到了馴服C++的韁繩。
本質(zhì)上是指針,這一點即使大家沒有看反匯編應(yīng)該也是猜到了。
對象在內(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: };
一個Father對象里只包含 (低地址 –> 高地址) : iA_,iB_。也就是一個Father對象的大小是8個字節(jié),函數(shù)并不會占用內(nèi)存空間。
為什么不會?
其實類的成員函數(shù)可以看做本質(zhì)上與普通函數(shù)相同。
編譯器在編譯的時候就知道函數(shù)的位置,所以調(diào)用普通函數(shù)的時候會直接 call 函數(shù)地址(偏移)。也就是被硬編碼了,函數(shù)的地址是固定的( 不考慮重定位之類的情況 )。
而成員函數(shù)的調(diào)用也是如此,只是編譯器還多做了一件事情,就是判斷這個對象有沒有調(diào)用這個函數(shù)的“權(quán)限”(函數(shù)不是你聲明的,當然無權(quán)調(diào)用),“權(quán)限”不夠就會報錯,告訴那個對象類型沒有這個方法。
所以,類對象的大小與這個類的方法數(shù)多少是沒關(guān)系的。成員函數(shù)和普通函數(shù)本質(zhì)上一樣,實現(xiàn)這個機制,要靠編譯器來做工作。
this指針:
成員函數(shù)與普通函數(shù)不同之處之一就是訪問對象的數(shù)據(jù)。
要訪問一個對象的元素,說白了就是要找到這個元素所在的內(nèi)存位置,也就是要有指針。
我們沒有看到傳遞this指針,因為這件事又是編譯器幫我們做了。
反匯編會看到對象調(diào)用一個方法的時候,會將這個對象的首部地址賦值給ecx寄存器,通過寄存器來傳遞this指針。
我們在成員函數(shù)里可以不需明寫this指針地調(diào)用對象元素,還是因為編譯器幫我們多做了一步“翻譯”。
私有化:
不多說,就是編譯器在編譯階段通過源碼來判斷某個元素是不是能夠被訪問,某個方法是不是能夠被調(diào)用,運行的時候并不會有訪問限制?创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_是私有的,但是還是被外界修改了。因為編譯器無法知道我干了這事(顯式的 oA.iA_ = 1 就被發(fā)現(xiàn)了哈)
構(gòu)造與析構(gòu):
說道底還是編譯器幫我們在多做了一些工作,生成了一些額外代碼。
需要注意的是:
1: void Test( Father oP )
2: {
3: }
4:
5: int main()
6: {
7: Father oA;
8: Test(oA);
9: return 0;
10: }
會調(diào)用拷貝構(gòu)造函數(shù)。
重載:
一樣還是編譯器的功勞,C++最后生成的函數(shù)名是與參數(shù)有關(guān)的,所以又不同參數(shù)的函數(shù)最后生成的函數(shù)名不同,看似同名,實則不同。在函數(shù)調(diào)用的時候,編譯器會判斷參數(shù)的類型,相應(yīng)的可以生成一個函數(shù)名進行“匹配”。( 當然不止這么簡單,還會考慮發(fā)生類型轉(zhuǎn)換的情況 )
繼承:
從內(nèi)存布局的角度上看
1: struct Child : Father
和
1: struct Child
2: {
3: Father o;
4: //other
5: };
相同(虛函數(shù)情況后面討論)。子類的前面部分和父類是一樣的。
所以一個接受 Father * 參數(shù)的函數(shù)可以接受 Child *參數(shù),而且轉(zhuǎn)換是安全的。
有 Father & 類型參數(shù)的函數(shù)可以接受 Child &,但是繼承方式要public。But , why ?
protected和private繼承模式,子類繼承的父類的接口對外都是隱藏的,所以以一個Father &傳入的參數(shù)所有的方法元素原則上是不可用的,用了肯定是違反規(guī)則的,編譯器判定這一點,所以報錯。
虛函數(shù):
比較特別的是這個。
Question:為什么需要虛函數(shù)?
網(wǎng)上看到的答案:基類可以通過虛函數(shù)對子類的相識功能進行管理。(我的C++primer被借走以后就此失蹤,所以只能網(wǎng)上找了)。
虛函數(shù)具體怎么回事就不細說了,討論一下背后的機制。
為了能夠?qū)崿F(xiàn)虛函數(shù),每個有虛函數(shù)的類有一張對應(yīng)的虛表。這個虛表儲存在只讀內(nèi)存區(qū),記錄了對應(yīng)函數(shù)的地址。(PS:一個類就只有一個虛表)
每個類對象都要保存一個虛表指針,保存本類的虛表地址。所以你使用 Father *指針指向一個Child對象,調(diào)用的虛函數(shù)是Child的。
虛表指針保存在每個對象的首部。
1: class Child : Father
2: {
3: int iC_;
4: void FuncC();
5: virtual void VF();
6: };
現(xiàn)在這個Child對象較前面的多了四個字節(jié)。內(nèi)存布局(從低地址到高地址)是:虛表指針__vfptr,iA_,iB_,iC_。
好。問題來了,Child繼承了Father,但是Father的函數(shù)并沒有為Child再量身定做一次,也就是說無論是Father對象還是Child對象,他們調(diào)用FuncA()都是同一個函數(shù)。但是Father并沒有__vfptr,Child對象在頭部多了這個,F(xiàn)uncA()中用this指針定位iA_和iB_不是都不正確嗎?
現(xiàn)象告訴我們FuncA()是可以正確訪問iA_和iB_,所以推測Child對象在調(diào)用FuncA的時候,傳的不是真正的首部地址,而是往后偏移了四個字節(jié)。
反匯編,確實如此。這么說Father類里不能調(diào)用虛函數(shù)了?當然,F(xiàn)ather都還不知道虛函數(shù)這回事,怎么在FuncA中調(diào)用。
還有一個有趣的現(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é)果啊,買定離手。
結(jié)果是:CB CB CC CC
在43行的地方,修改了oB的虛表指針,讓其指向CC類的虛表。
但是oB.ShowID()沒理會我們的修改,還是調(diào)用CB類的ShowID。反匯編,發(fā)現(xiàn)他沒走“獲取虛表指針,在虛表中得到相應(yīng)的函數(shù)地址”這一套,直接調(diào)用了。因為一般人不會閑著蛋疼去改對象的虛表指針的,對象的類型是明確的,編譯器可以通過這些信息確定調(diào)用的函數(shù)地址,所以沒必要走他一套,這樣效率還更高。
而pCB->ShowID()就不同了,他很乖地地走了流程,因為一個父類指針可以指向一個子類對象,編譯器無法找信息,所以走流程。
那現(xiàn)在糾結(jié)了,為神馬 ((CB*)(&oB))->ShowID() 輸出CB。
反匯編看,發(fā)現(xiàn)編譯器又擅自做主,沒有走指針的流程。
那你猜猜((Base*)(&oB))->ShowID();輸出的是什么?CC。
比較二者的差異,可以大概發(fā)現(xiàn)一些端倪,什么時候走流程,什么時候不走。
最后是Test(oB)了,前面說過引用的本質(zhì)是指針,所以這個結(jié)果很好理解。
還有,想過
1: void Test2( Base oP )
2: {
3: oP.ShowID();
4: }
拷貝的時候有沒有拷貝虛表指針嗎?試試就知道,厄…發(fā)現(xiàn)沒有。
前面說過這樣會調(diào)用拷貝構(gòu)造函數(shù),但是你在這個函數(shù)你沒有寫虛表指針的賦值。但是邪惡的編譯器已經(jīng)幫你悄悄加上去了哈哈哈哈~。(唉?節(jié)操呢)
RTTI
每個類有特定的虛表地址,每個對象會保存這個虛表地址,應(yīng)該想到了吧,偷懶,不寫了。
綜上?梢钥吹剑嫦?qū)ο髾C制在底層并不特別,機制的實現(xiàn)主要靠的是編譯器。