• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            閱讀本文前最好已經(jīng)讀過 理解程序內(nèi)存 和 理解C++變量存儲模型 相關(guān)的內(nèi)容, C++對象模型比較經(jīng)典的書是《深度探索C++對象模型》, 但是書本的知識始終局限在理論上,熟話說“紙上得來終覺淺”,只有我們自已用工具經(jīng)過驗(yàn)證,我們才能真正的理解這些知識。下面我們用WinDbg為工具對C++對象模型進(jìn)行探索。


            類對象實(shí)例究竟包含哪些東西

            我們的例子代碼非常簡單:
            #include <iostream>

            using namespace  std;

            class A
            {
            public:
                void fun1(){ cout << "fun1"; }
                virtual void fun2() { cout << "fun2"; }
                virtual ~A() {}

                char m_cA;
                int m_nA;
                static int s_nCount;
            };

            int A::s_nCount = 0;

            int main() 
            {
                A* p = new A;
                p->fun2();

                system("pause");

                return 0;
            }
            我們在main函數(shù)里 system("pause");的地方設(shè)置斷點(diǎn),然后讓程序運(yùn)行到這里。

            輸入WinDbg命令?? sizeof(*p)讓他打印A對象的大小,輸出如下:
             0:000> ?? sizeof(*p)
            unsigned int 0xc
            可以看到A的實(shí)例對象大小是 0xc = 12 字節(jié)

            接下來輸入WinDbg命令dt p讓他打印p所指下對象的內(nèi)存布局, 輸出如下:
            0:000> dt p
            Local var @ 0x13ff74 Type A*
            0x00034600 
               +0x000 __VFN_table : 0x004161d8 
               +0x004 m_cA             : 120 'x'
               +0x008 m_nA             : 0n0
               =0041c3c0 A::s_nCount      : 0n0
            可以看到A的對象實(shí)例由虛表指針,m_cA, m_nA組成,正好是12字節(jié)(內(nèi)部char作了4字節(jié)對齊)。

            最后一個靜態(tài)變量s_nCount的地址是0041c3c0, 我們可以通過命令!address 0041c3c0查看它所在地址的屬性, 結(jié)果如下:
            0:000> !address 0041c3c0
            Usage:                  Image
            Allocation Base:        00400000
            Base Address:           0041b000
            End Address:            0041f000
            Region Size:            00004000
            Type:                   01000000    MEM_IMAGE
            State:                  00001000    MEM_COMMIT
            Protect:                00000004    PAGE_READWRITE
            More info:              lmv m ConsoleTest
            More info:              !lmi ConsoleTest
            More info:              ln 0x41c3c0
            可以看到類靜態(tài)變量被編譯在consoletest.exe可執(zhí)行文件的 可讀寫數(shù)據(jù)節(jié)(.data)

            結(jié)論: C++中類實(shí)例對象由虛表指針和成員變量組成(一般最開始的4個字節(jié)是虛表指針),而類靜態(tài)變量分布在PE文件的.data節(jié)中,與類實(shí)例對象無關(guān)。


            虛表位置和內(nèi)容

            根據(jù)+0x000 __VFN_table : 0x004161d8  繼續(xù)上面的調(diào)試,我們看到虛表地址是在0x004161d8, 輸入!address 0x004161d8, 查看虛表地址的屬性:
            0:000> !address 0x004161d8
            Usage:                  Image
            Allocation Base:        00400000
            Base Address:           00416000
            End Address:            0041b000
            Region Size:            00005000
            Type:                   01000000    MEM_IMAGE
            State:                  00001000    MEM_COMMIT
            Protect:                00000002    PAGE_READONLY
            More info:              lmv m ConsoleTest
            More info:              !lmi ConsoleTest
            More info:              ln 0x4161d8
            可以看到類虛表被編譯在consoletest.exe可執(zhí)行文件的 只讀數(shù)據(jù)節(jié)(.rdata)

            接下來我們看下虛表中有哪些內(nèi)容, 輸入dps 0x004161d8 查看虛表所在地址的符號,輸出如下:
            0:000> dps 0x004161d8 
            004161d8  00401080 ConsoleTest!A::fun2 [f:\test\consoletest\consoletest\consoletest.cpp @ 13]
            004161dc  004010a0 ConsoleTest!A::`scalar deleting destructor'
            004161e0  326e7566
            004161e4  00000000
            我們可以看到虛表里正好包含了我們的2個虛函數(shù)fun2()和~A().

            另外我們也可以多new幾個A的實(shí)例試下,我們可以看到他們的虛表地址都是 0x004161d8。

            結(jié)論: C++中類的虛表內(nèi)容由虛函數(shù)地址組成,虛表分布在PE文件的.rdata節(jié),并且同一類的所有實(shí)例共享同一個虛表。


            禁止生成虛表會怎樣

            我們可以通過__declspec(novtable)來告訴編譯器不要生成虛表,ATL中大量應(yīng)用這種技術(shù)來減小虛表的內(nèi)存開銷,我們原來的代碼改成
            class __declspec(novtable) A
            {
            public:
                void fun1(){ cout << "fun1"; }
                virtual void fun2() { cout << "fun2"; }
                virtual ~A() {}

                char m_cA;
                int m_nA;
                static int s_nCount;
            };
            繼續(xù)原來的方法調(diào)試,我們會看到一運(yùn)行到p->fun2(), 程序就會Crash, 究竟是什么原因?
            用原來的?? sizeof(*p)命令,可以看到對象大小依然是12 字節(jié), 輸入dt p, 輸出:
            0:000> dt p
            Local var @ 0x13ff74 Type A*
            0x00033e58 
               +0x000 __VFN_table : 0x00030328 
               +0x004 m_cA             : 40 '('
               +0x008 m_nA             : 0n0
               =0040dce0 A::s_nCount      : 0n0
            從上面可以看到虛表似乎依然存在, 但是再輸入dps 0x00030328 查看虛表內(nèi)容, 你就會發(fā)現(xiàn)現(xiàn)在虛表內(nèi)容果然已經(jīng)不存在了:
            0:000> dps 0x00030328 
            00030328  00030328
            0003032c  00030328
            00030330  00030330
            但是我們的程序還是通過虛表去調(diào)用虛函數(shù)fun2, 難怪會Crash了。

            結(jié)論: 通過__declspec(novtable),我們只能禁止編譯器生成虛表,但是不能阻止對象仍包含虛表指針(不能減小對象的大小),也不能阻止程序?qū)μ摫淼脑L問(盡管實(shí)際虛表不存在),所以禁止生成虛表只適用于永遠(yuǎn)不會實(shí)例化的類(基類)


            單繼承對象內(nèi)存模型

            下面我們簡單的將上面的代碼改下下,讓B繼承A,并且重寫原來的虛函數(shù)fun2:
            #include <iostream>

            using namespace  std;

            class  A
            {
            public:
                void fun1(){ cout << "fun1"; }
                virtual void fun2() { cout << "fun2"; }
                virtual ~A() {}

                char m_cA;
                int m_nA;
                static int s_nCount;
            };

            int A::s_nCount = 0;

            class B: public A
            {
            public:
                virtual void fun2() { cout << "fun2 in B"; }
                virtual void fun3() { cout << "fun3 in B"; }

            public:
                int m_nB;
            };

            int main() 
            {
                B* p = new B;
                A* p1 = p;

                p1->fun2();

                system("pause");

                return 0;
            }
            用原來的方法進(jìn)行調(diào)試,查看B對象的內(nèi)存布局
            0:000> dt p
            Local var @ 0x13ff74 Type B*
            0x00034640 
               +0x000 __VFN_table : 0x004161d8 
               +0x004 m_cA             : 120 'x'
               +0x008 m_nA             : 0n0
               =0041c3e0 A::s_nCount      : 0n0
               +0x00c m_nB             : 0n0
            可以看到B對象的大小是原來A對象的大小加4(m_nB), 總共是16字節(jié),查看B的虛表內(nèi)容如下:
            0:000> dps 0x004161d8 
            004161d8  00401080 ConsoleTest!B::fun2 [f:\test\consoletest\consoletest\consoletest.cpp @ 26]
            004161dc  004010c0 ConsoleTest!B::`scalar deleting destructor'
            004161e0  004010a0 ConsoleTest!B::fun3 [f:\test\consoletest\consoletest\consoletest.cpp @ 27]
            004161e4  326e7566
            可以看到虛表中保存的都是B的虛函數(shù)地址: fun2(), ~B(), fun3()

            結(jié)論: 單繼承時父類和子類共用同一虛表指針,而子類的數(shù)據(jù)被添加在父類數(shù)據(jù)之后,父類和子類的對象指針在相互轉(zhuǎn)化時值不變。


            多繼承對象內(nèi)存模型

            我們把上面的代碼改成多繼承的方式, class A, class B, 然后C繼承A和B:
            #include <iostream>
            using namespace  std;
            class  A
            {
            public:
            virtual void fun()  {cout << "fun in A";}
            virtual void funA() {cout << "funA";}
            virtual ~A() {}
            char m_cA;
            int m_nA;
            static int s_nCount;
            };
            int A::s_nCount = 0;
            class B
            {
            public:
            virtual void fun() {cout << "fun in B";}
            virtual void funB() {cout << "funB";}
            int m_nB;
            };
            class C: public A, public B 
            {
            public:
            virtual void fun() {cout << "fun in C";};
            virtual void funC(){cout << "funC";}
            int m_nC;
            };
            int main() 
            {
            C* p = new C;
            B* p1 = p;
            p->fun();
            system("pause");
            return 0;
            }
            依舊用原來的方式調(diào)試,查看C的內(nèi)存布局
            0:000> dt p
            Local var @ 0x13ff74 Type C*
            0x00034600 
               +0x000 __VFN_table : 0x004161f4 
               +0x004 m_cA             : 120 'x'
               +0x008 m_nA             : 0n0
               =0041c4a0 A::s_nCount      : 0n0
               +0x00c __VFN_table : 0x004161e8 
               +0x010 m_nB             : 0n0
               +0x014 m_nC             : 0n0
            可以看到C對象由0x18 = 24字節(jié)組成,可以看到數(shù)據(jù)依次是虛表指針,A的數(shù)據(jù),虛表指針, B的數(shù)據(jù), C的數(shù)據(jù)。

            查看第一個虛表內(nèi)容:
            0:000> dps 0x004161f4 
            004161f4  004010f0 ConsoleTest!C::fun [f:\test\consoletest\consoletest\consoletest.cpp @ 33]
            004161f8  004010b0 ConsoleTest!A::funA [f:\test\consoletest\consoletest\consoletest.cpp @ 16]
            004161fc  00401130 ConsoleTest!C::`scalar deleting destructor'
            00416200  00401110 ConsoleTest!C::funC [f:\test\consoletest\consoletest\consoletest.cpp @ 34]
            可以看到前面虛表的前面3個虛函數(shù)符合A的虛表要求(第一個A::fun讓C::fun取代了,第三個A的析構(gòu)函數(shù)~A讓~C取代了),最后加上了C的新增虛函數(shù)funC, 所以該虛表同時符合A和C的要求,也就是說A和C共用同一個虛表指針。

            再看第二個虛表內(nèi)容:
            0:000> dps 0x004161e8 
            004161e8  00402850 ConsoleTest![thunk]:C::fun`adjustor{12}'
            004161ec  004010d0 ConsoleTest!B::funB [f:\test\consoletest\consoletest\consoletest.cpp @ 27]
            004161f0  004187a0 ConsoleTest!C::`RTTI Complete Object Locator'
            可以看到第二個虛表符合B的虛表要求,并且把B的虛函數(shù)fun用C的改寫了,所以它是給B用的。 

            我們再看基類對象B的布局情況:
            0:000> dt p1
            Local var @ 0x13ff70 Type B*
            0x0003460c 
               +0x000 __VFN_table : 0x004161e8 
               +0x004 m_nB             : 0n0
            我們可以看到p1指針本身在堆棧上的地址是0x13ff70,而p1所指向?qū)ο蟮牡刂肥?/span>0x003e460c ,所以將C指針轉(zhuǎn)成B指針后,B的地址和C的地址之差是0x003e460c0x00034600  = 0xc = 12字節(jié), 也就是說B的指針p1指向我們上面的第二個虛表指針。

            另外我們上面要特別留意第二個虛表的第一個函數(shù):004161e8  00402850 ConsoleTest![thunk]:C::fun`adjustor{12}'
            我們發(fā)現(xiàn)這個函數(shù)不是我們真正的class C的fun函數(shù):004161f4  004010f0 ConsoleTest!C::fun [f:\test\consoletest\consoletest\consoletest.cpp @ 33]
            該函數(shù)地址是00402850, 我們可以反匯編看下:
            0:000> u 00402850
            ConsoleTest![thunk]:C::fun`adjustor{12}':
            00402850 83e90c          sub     ecx,0Ch
            00402853 e998e8ffff      jmp     ConsoleTest!C::fun (004010f0)
            00402858 cc              int     3
            00402859 cc              int     3
            0040285a cc              int     3
            0040285b cc              int     3
            0040285c cc              int     3
            0040285d cc              int     3
            可以看到這個函數(shù)是編譯器生成的一個代理函數(shù),它內(nèi)部實(shí)現(xiàn)只是把我們B的this指針(ecx)加上12個字節(jié)的偏移后,然后再去調(diào)用我們真正的C的fun函數(shù)。
            為什么會這樣呢? 因?yàn)閏lass C的fun 內(nèi)部在實(shí)現(xiàn)時假設(shè)的this指針都是它本身實(shí)例的起始地址,但是B指針并不符合這個要求,所以B的指針需要調(diào)整后才能去調(diào)用真正C的方法。

            結(jié)論: 多重繼承時派生類和第一個基類公用一個虛表指針,他們的對象指針相互轉(zhuǎn)化時值不變;而其他基類(非第一個)和派生類的對象指針在相互轉(zhuǎn)化時有一定的偏移,他們內(nèi)部虛表保存的函數(shù)指針并不一定是最終的實(shí)現(xiàn)的虛函數(shù)(可能是類似上面的一個代理函數(shù))。



            如何用虛表實(shí)現(xiàn)多態(tài)?

            有了上面這些分析,這個咱們就不證明了,直接下結(jié)論吧。

            結(jié)論: C++通過虛表來實(shí)現(xiàn)多態(tài),派生類的虛表和基類的虛表根據(jù)索引依次保存相同的函數(shù)類型指針,但是這些函數(shù)指針最終指向他們各自最終的實(shí)現(xiàn)函數(shù),調(diào)用虛函數(shù)時,我們只是根據(jù)函數(shù)所在虛表的索引來調(diào)用,所以他們可以在派生類中有各自不同的實(shí)現(xiàn)。 



            虛擬繼承

            恩,有了前面的基礎(chǔ),這個就當(dāng)思考題吧...


            總之,拿著一把刀,庖丁解牛般的剖析語言背后的實(shí)現(xiàn)細(xì)節(jié),看起來不是那么實(shí)用,但是它能讓你對語言的理解更深刻。實(shí)際上ATL中大量應(yīng)用上面的技術(shù),如果沒有對C++ 對象模型有比較深刻的理解,是很難深入下去的。
            posted on 2012-09-21 23:02 Richard Wei 閱讀(4163) 評論(2)  編輯 收藏 引用 所屬分類: C++

            FeedBack:
            # re: 探索C++對象模型
            2015-08-18 13:32 | lvshiling@qq.com
            可以看到這個函數(shù)是編譯器生成的一個代理函數(shù),它內(nèi)部實(shí)現(xiàn)只是把我們B的this指針(ecx)加上12個字節(jié)的偏移后,然后再去調(diào)用我們真正的C的fun函數(shù)。

            應(yīng)修改為

            可以看到這個函數(shù)是編譯器生成的一個代理函數(shù),它內(nèi)部實(shí)現(xiàn)只是把我們B的this指針(ecx)減去12個字節(jié)的偏移后,然后再去調(diào)用我們真正的C的fun函數(shù)。  回復(fù)  更多評論
              
            久久青青国产| 亚洲综合久久久| 成人妇女免费播放久久久| 亚洲国产精品无码久久久秋霞2| 国产aⅴ激情无码久久| 久久久久亚洲av无码专区导航| 久久―日本道色综合久久| 久久99精品国产麻豆婷婷| 一级女性全黄久久生活片免费| 久久人人爽人人爽人人AV| 亚洲成色999久久网站| 18禁黄久久久AAA片| 久久最近最新中文字幕大全| 久久久久99这里有精品10| 国产午夜福利精品久久2021| 思思久久99热免费精品6| 99久久无码一区人妻a黑| 无码任你躁久久久久久老妇App| 国产精品久久成人影院| 久久亚洲AV无码精品色午夜| 精品无码久久久久久久动漫| 久久综合噜噜激激的五月天| 久久夜色精品国产亚洲av| 亚洲国产精品婷婷久久| 日韩精品无码久久久久久| 亚洲国产成人久久笫一页| 久久精品成人欧美大片| 久久久久中文字幕| 青青草国产精品久久| 精品国产福利久久久| 国产精品对白刺激久久久| 伊人久久大香线蕉av不变影院| 久久乐国产精品亚洲综合| 国产午夜福利精品久久| 婷婷久久综合九色综合98| 久久精品嫩草影院| 亚洲国产精品久久久久婷婷老年| 久久精品无码一区二区无码| 精品久久久久香蕉网| 77777亚洲午夜久久多喷| 久久99热国产这有精品|