所有的分析均針對|gcc version 4.3.4 [gcc-4_3-branch revision 152973] (SUSE Linux X86_64)|這一版本。
最簡單的類
先從一個簡單的類開始吧。如下,此簡單類,非常簡單,兩個int成員,通過printf很容易瞭解到它的內存佈局,本質就是一個C結構體,兩個成員依次排列。
對象:|成員1 | 成員2 |
1: #include <cstdio>
2: class Class0
3: {
4: public:
5: int member1;
6: int member2;
7: };
8: int main()
9: {
10: Class0 c;
11: printf("object addr=0x%lx\nmember1 addr=0x%lx\nmember2 addr=0x%lx\n",
12: &c, &c.member1, &c.member2);
13: return 0;
14: }
# ./a.out
object addr=0x7fffea480d70
member1 addr=0x7fffea480d70 //類成員1
member2 addr=0x7fffea480d74 //類成員2
成員函數
那麼我們增加點複雜性,添加一個成員函數。
1: #include <cstdio>
2: class Class1
3: {
4: public:
5: int member1;
6: int member2;
7: void function1() { printf("Class1::function1"); }
8: };
9: int main()
10: {
11: Class1 c;
12: printf("object addr=0x%lx\n", &c);
13: printf("member1 addr=0x%lx\n", &c.member1);
14: printf("member1 addr=0x%lx\n", &c.member2);
15: printf("function1 addr=0x%lx\n", &Class1::function1);
16: return 0;
17: }
./a.out
object addr =0x7fff6805bf90
member1 addr=0x7fff6805bf90
member1 addr=0x7fff6805bf94
function1 addr=0x4006a0//成員函數地址在代碼段。----這簡直是廢話,不在代碼段沒法玩啊。
代碼段: |成員函數|
我們看到,對象數據成員的佈局並沒有變化,但是函數成員的地址跑到十萬八千里之外了。爲什麼?很簡單,因爲函數是代碼,放在了代碼段。這也是我們通過Class1::function1來取值,而不是c.function1的原因。從這裏可以看出,類的函數成員本質就是一個C全局函數,那麼如果函數內訪問類的非靜態數據成員,如何動態的獲取成員地址?編譯器是這樣做的:
1. 編譯器生成function1()的指令時,如果遇到了訪問對象的數據成員,比如member1,就從一個約定的位置(比如一個寄存器)獲取對象的首地址(其實就是this指針),然後加上偏移(這個是編譯時期可以確定的),也就找到了member1對應的內存位置,就可以訪問member1了。
2. 編譯器生成c.function1()對應的指令時,把c的地址,放到了上述約定的位置。
簡單來說,c.function1() 等價於function1(c), c是作爲隱含參數傳遞給function1了。
虛函數
好了搞清楚了成員函數的工作機制,我們再進一步分析,如下例子,有了繼承,並且基類成員函數是一個虛函數。派生類重載了它。
1: #include <cstdio>
2: class Base
3: {
4: public:
5: virtual void function() { printf("Base::function1\n"); }
6: };
7: class Derived : public Base
8: {
9: public:
10: void function() { printf("Derived::function1\n"); }
11: };
12: int main()
13: {
14: printf("Base::function addr = 0x%lx\n", &Base::function);
15: printf("Derived::function addr = 0x%lx\n", &Derived::function);
16: Base* pb = new Derived();
17: pb->function();
18: return 0;
19: }
./a.out
Base::function addr = 0x1
Derived::function addr = 0x1
Derived::function1
1: Base* pb = new Derived();
2: pb->function();
派生類指針賦給了基類指針,調用function,但執行還是派生類的function,這就是多態了。那麼對於 pb->function(); 這個語句來說,編譯器是不能夠在編譯時期決定調用哪個function的。因爲它並不知道pb這個指針是通過派生類轉化而來。大家會說,我們上面的語句不是告訴它了嗎?這個肯定不行,編譯器不能做這個上下文關聯,你要是通過函數參數傳遞過來,賦值的地方離這條語句很遠甚至都不在一個源文件裏面怎麼辦?所以這個決定調用哪個function的信息,必須保存在內存裏面,運行期間就可以執行正確的函數。那麼具體保存在哪裏?如何工作的?gcc是這樣做的:
1. 申請一段內存,存放虛函數的地址。就是一些書上所說的虛表。本質就是一個數組。
2. 在對象的起始位置,存放虛表首地址,而不是像普通類對象那樣存放第一個非靜態數據成員。
3. pb->function(); 這條語句執行時,編譯器知道function是一個虛函數(我們聲明瞭virtual關鍵字),那麼就會採用虛函數的調用方法,首先根據pb找到虛表的首地址,然後加上一個偏移量,因爲是編譯器把function這個函數的地址放到虛表內的,所以它知道偏移量。我們通過下面這段代碼驗證這點:
1: #include <cstdio>
2: class Base
3: {
4: public:
5: virtual void function1() { printf("Base::function1\n"); }
6: virtual void function2() { printf("Base::function2\n"); }
7: };
8: int main()
9: {
10: printf("Base::function addr = 0x%lx\n", &Base::function1);
11: printf("Base::function addr = 0x%lx\n", &Base::function2);
12: Base* pb = new Base;
13: long* vtl = *(long**)pb;
14: printf("0x%lx\n", *(vtl));
15: printf("0x%lx\n", *(vtl + 1));
16: return 0;
17: }
# ./a.out
Base::function addr = 0x1
Base::function addr = 0x9
0x40082a
0x400812
# nm a.out | grep function
000000000040082a W _ZN4Base9function1Ev
0000000000400812 W _ZN4Base9function2Ev
# c++filt _ZN4Base9function1Ev
Base::function1()
# c++filt _ZN4Base9function2Ev
Base::function2()
虛表: |虛函數1的地址|虛函數2的地址|
代碼段: |虛函數1|虛函數2|
1. 通過long* vtl = *(long**)pb; 獲取pb對象第一個成員的內容,我們拿到了虛表的首地址vtl。
2. printf("0x%lx\n", *(vtl)); 訪問虛表的第一個元素,打印的是0x40082a,恰好對應我們通過nm查看到的Base::function1的函數地址000000000040082a 。
3. printf("0x%lx\n", *(vtl + 1)); 訪問虛表的第二個元素,打印的是0x400812,恰好對應我們通過nm查看到的Base::function2的函數地址0000000000400812 。
那麼&Base::function1是0x1,&Base::function2是0x9,何解?其實怎麼解讀,完全看編譯器心情。。。從我們的實驗結果來看,gcc是把它解讀成了虛表偏移量+1。編譯器也是可以解讀爲函數的真實地址的。
所謂多態,也就是這麼回事兒,其邏輯並不複雜,只是C++"封裝"了細節,只給我們展示了它的強大形象,讓我們覺得多態好神奇啊,其實丫的,本質就是函數指針,就是地址而已,因爲地址纔是CPU理解的東西。懂得這點,就知道內核裏到處都是多態,同樣是一個read操作,read不同的文件,執行不同的函數。。。內核就是在文件對象(C結構體)裏,保存了函數指針,不同的文件系統註冊不同的函數指針。內核是各種編程技術、思想的集大成者,OO思想隨處可見。
單繼承
1: class Base
2: {
3: public:
4: int b;
5: };
6: class Derived : public Base
7: {
8: public:
9: int d;
10: };
11: int main()
12: {
13: Derived d;
14: d.b = 2012;
15: Base* b = &d;
16: b->b = 2012;
17: }
1: #include <cstdio>
2: class Base
3: {
4: public:
5: int b;
6: };
7: class Derived : public Base
8: {
9: public:
10: int d;
11: };
12: int main()
13: {
14: Derived d;
15: printf("Derived = 0x%lx\n", &d);
16: printf("Derived.b = 0x%lx\n", &d.b);
17: printf("Derived.d = 0x%lx\n", &d.d);
18: return 0;
19: }
# ./a.out
Derived = 0x7fffece35bf0
Derived.b = 0x7fffece35bf0
Derived.d = 0x7fffece35bf4
對象: | 基類的成員|派生類的成員|
多繼承
單繼承的內存佈局,是基類成員在前,派生在後,但是多繼承呢?丫的有兩個基類,誰前誰後?誰前誰後不重要,關鍵的是根據上面單繼承分析,如果基類成員在派生類對象的位置不是從頭開始,派生類對像指針轉化爲基類指針之後,就不能正確訪問基類成員了。而多繼承,必然至少有一個基類不是從頭開始的。那麼怎麼辦?還能怎麼辦,涼拌!當你把一個派生類對象地址賦值給一個基類指針,如果這個基類在派生類中的位置,不是從頭開始的,編譯器偷偷的把它改變,加上基類在派生類中的位置偏移量!我們來驗證下:
1: #include <cstdio>
2: class Base1
3: {
4: public:
5: int b1;
6: };
7: class Base2
8: {
9: public:
10: int b2;
11: };
12: class Derived : public Base1, public Base2
13: {
14: public:
15: int d;
16: };
17: int main()
18: {
19: Derived d;
20: printf("Derived = 0x%lx\n", &d);
21: printf("Derived.b1 = 0x%lx\n", &d.b1);
22: printf("Derived.b2 = 0x%lx\n", &d.b2);
23: printf("Derived.d = 0x%lx\n", &d.d);
24: Base2* b2p = &d;
25: printf("Base2 pointer = 0x%lx\n", b2p);
26: return 0;
27: }
# ./a.out
Derived = 0x7fffedfe10e0
Derived.b1 = 0x7fffedfe10e0
Derived.b2 = 0x7fffedfe10e4
Derived.d = 0x7fffedfe10e8
Base2 pointer = 0x7fffedfe10e4
可以看到,擺放的順序是Base1,Base2,Derived:
對象:| 基類1的成員 | 基類2的成員 | 派生類的成員
而當我們把Derived的地址0x7fffedfe10e0賦給Base2時,變成了0x7fffedfe10e4,即Base2成員的起始位置,這樣我們的b2p->b2; 可以正確的工作。是不是很神奇?=號都是不可信的!
多繼承+虛函數
對象:| 虛表1的地址 | 基類1的成員 | 虛表2的地址 | 基類2的成員 | 派生類的成員
其中虛表1中存放是派生類重載的虛函數地址,無論來自於基類1還是基類2。虛表2只存放基類2的重載函數地址(實際上GCC幫你生成了一箇中間函數,中間函數再去調用實際的函數)。
1: #include <cstdio>
2: class Base1
3: {
4: public:
5: int b1;
6: virtual void function1() { printf("Base1::function1\n"); }
7: };
8: class Base2
9: {
10: public:
11: int b2;
12: virtual void function2() { printf("Base2::function2\n"); }
13: };
14: class Derived : public Base1, public Base2
15: {
16: public:
17: int d;
18: void function1() { printf("Derived::function1\n"); }
19: void function2() { printf("Derived::function2\n"); }
20: };
21: int main()
22: {
23: Derived d;
24: printf("Derived = 0x%lx\n", &d);
25: printf("Derived.b1 = 0x%lx\n", &d.b1);
26: printf("Derived.b2 = 0x%lx\n", &d.b2);
27: printf("Derived.d = 0x%lx\n", &d.d);
28: Base2* b2p = &d;
29: printf("Base2 pointer = 0x%lx\n", b2p);
30: long* vtl = *(long**)b2p;
31: printf("0x%lx\n", *(vtl));
32: printf("0x%lx\n", *(vtl + 1));
33: vtl = *(long**)&d;
34: printf("0x%lx\n", *(vtl));
35: printf("0x%lx\n", *(vtl + 1));
36: return 0;
37: }
# ./a.out
Derived = 0x7fffa74ae400
Derived.b1 = 0x7fffa74ae408//b1沒有放在最開始,因爲第一個是虛表地址
Derived.b2 = 0x7fffa74ae418//b2沒有放在b1後面,因爲前邊還有一個虛表地址
Derived.d = 0x7fffa74ae41c
Base2 pointer = 0x7fffa74ae410//base2在派生類中的起始位置,
0x4008aa//虛表2中存放的函數地址,gcc生成的中間函數
0x0//虛表2中存放的函數地址
0x4008c8//虛表1中存放的函數地址,function1
0x4008b0//虛表1中存放的函數地址,function2
# nm a.out |grep function
00000000004008e0 W _ZN5Base19function1Ev
00000000004008f8 W _ZN5Base29function2Ev
00000000004008c8 W _ZN7Derived9function1Ev
00000000004008b0 W _ZN7Derived9function2Ev
00000000004008aa W _ZThn16_N7Derived9function2Ev
# c++filt _ZN7Derived9function1Ev _ZN7Derived9function2Ev _ZThn16_N7Derived9function2Ev
Derived::function1()
Derived::function2()
non-virtual thunk to Derived::function2()
# objdump -d a.out | sed -n '/_ZThn16_N7Derived9function2Ev/,/00000/p'
00000000004008aa <_ZThn16_N7Derived9function2Ev>:
4008aa: 48 83 c7 f0 add $0xfffffffffffffff0,%rdi
4008ae: eb 00 jmp 4008b0 <_ZN7Derived9function2Ev>//中間函數跳轉到了function2
00000000004008b0 <_ZN7Derived9function2Ev>: