• <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>
            posts - 200, comments - 8, trackbacks - 0, articles - 0

            作者:July。
            出處:http://blog.csdn.net/v_JULY_v 。

             

            前奏

                有關(guān)虛函數(shù)的問題層出不窮,有關(guān)虛函數(shù)的文章千篇一律,那為何還要寫這一篇有關(guān)虛函數(shù)的文章呢?看完本文后,相信能懂其意義之所在。同時,原狂想曲系列已經(jīng)更名為程序員編程藝術(shù)系列,因為不再只專注于“面試”,而在“編程”之上了。ok,如果有不正之處,望不吝賜教。謝謝。


            第一節(jié)、一道簡單的虛函數(shù)的面試題
            題目要求:寫出下面程序的運行結(jié)果?

            1. //謝謝董天喆提供的這道百度的面試題   
            2. #include <iostream>  
            3. using namespace std;  
            4. class A{  
            5.   public:virtual void p()   
            6.   {   
            7.     cout << "A" << endl;   
            8.   }  
            9. };  
            10.   
            11. class B : public A  
            12. {  
            13.   public:virtual void p()   
            14.   { cout << "B" << endl;  
            15.   }  
            16. };  
            17.   
            18. int main()   
            19. {  
            20.   A * a = new A;  
            21.   A * b = new B;  
            22.   a->p();  
            23.   b->p();  
            24.   delete a;  
            25.   delete b;      
            26.   return 0;  
            27. }  

             

                我想,這道面試題應(yīng)該是考察虛函數(shù)相關(guān)知識的相對簡單的一道題目了。然后,希望你碰到此類有關(guān)虛函數(shù)的面試題,不論其難度是難是易,都能夠舉一反三,那么本章的目的也就達到了。ok,請跟著我的思路,咱們步步深入(上面程序的輸出結(jié)果為A B)。

             

             

            第二節(jié)、有無虛函數(shù)的區(qū)別
                  1、當(dāng)上述程序中的函數(shù)p()不是虛函數(shù),那么程序的運行結(jié)果是如何?即如下代碼所示:

            class A
            {
            public:
             void p() 
             { 
              cout << "A" << endl; 
             }
             
            };

            class B : public A
            {
            public:
             void p() 
             { 
              cout << "B" << endl;
             }
            };

            對的,程序此時將輸出兩個A,A。為什么?
            我們知道,在構(gòu)造一個類的對象時,如果它有基類,那么首先將構(gòu)造基類的對象,然后才構(gòu)造派生類自己的對象。如上,A* a=new A,調(diào)用默認構(gòu)造函數(shù)構(gòu)造基類A對象,然后調(diào)用函數(shù)p(),a->p();輸出A,這點沒有問題。
                然后,A * b = new B;,構(gòu)造了派生類對象B,B由于是基類A的派生類對象,所以會先構(gòu)造基類A對象,然后再構(gòu)造派生類對象,但由于當(dāng)程序中函數(shù)是非虛函數(shù)調(diào)用時,B類對象對函數(shù)p()的調(diào)用時在編譯時就已靜態(tài)確定了,所以,不論基類指針b最終指向的是基類對象還是派生類對象,只要后面的對象調(diào)用的函數(shù)不是虛函數(shù),那么就直接無視,而調(diào)用基類A的p()函數(shù)。

                  2、那如果加上虛函數(shù)呢?即如最開始的那段程序那樣,程序的輸出結(jié)果,將是什么?
            在此之前,我們還得明確以下兩點:
                a、通過基類引用或指針調(diào)用基類中定義的函數(shù)時,我們并不知道執(zhí)行函數(shù)的對象的確切類型,執(zhí)行函數(shù)的對象可能是基類類型的,也可能是派生類型的。
                b、如果調(diào)用非虛函數(shù),則無論實際對象是什么類型,都執(zhí)行基類類型所定義的函數(shù)(如上述第1點所述)。如果調(diào)用虛函數(shù),則直到運行時才能確定調(diào)用哪個函數(shù),運行的虛函數(shù)是引用所綁定的或指針所指向的對象所屬類型定義的版本。

            根據(jù)上述b的觀點,我們知道,如果加上虛函數(shù),如上面這道面試題,

            class A
            {
            public:
             virtual void p() 
             { 
              cout << "A" << endl; 
             }
             
            };

            class B : public A
            {
            public:
             virtual void p() 
             { 
              cout << "B" << endl;
             }
            };

            int main() 
            {
             A * a = new A;
             A * b = new B;
             a->p();
             b->p();
             delete a;
             delete b;
                return 0;
            }

             

            那么程序的輸出結(jié)果將是A B。

            所以,至此,咱們的這道面試題已經(jīng)解決。但虛函數(shù)的問題,還沒有解決。


            第三節(jié)、虛函數(shù)的原理與本質(zhì)
                我們已經(jīng)知道,虛(virtual)函數(shù)的一般實現(xiàn)模型是:每一個類(class)有一個虛表(virtual table),內(nèi)含該class之中有作用的虛(virtual)函數(shù)的地址,然后每個對象有一個vptr,指向虛表(virtual table)的所在。

            請允許我援引自深度探索c++對象模型一書上的一個例子:

            class Point { 
            public: 
               virtual ~Point();  

               virtual Point& mult( float ) = 0; 

               float x() const { return _x; }     //非虛函數(shù),不作存儲
               virtual float y() const { return 0; }  
               virtual float z() const { return 0; }  
               // ...

            protected: 
               Point( float x = 0.0 ); 
               float _x; 
            };

                  1、在Point的對象pt中,有兩個東西,一個是數(shù)據(jù)成員_x,一個是_vptr_Point。其中_vptr_Point指向著virtual table point,而virtual table(虛表)point中存儲著以下東西:

            • virtual ~Point()被賦值slot 1,
            • mult() 將被賦值slot 2.
            • y() is 將被賦值slot 3
            • z() 將被賦值slot 4.

            class Point2d : public Point { 
            public: 
               Point2d( float x = 0.0, float y = 0.0 )  
                  : Point( x ), _y( y ) {} 
               ~Point2d();   //1

               //改寫base class virtual functions 
               Point2d& mult( float );  //2
               float y() const { return _y; }  //3

            protected: 
               float _y; 
            };

                  2、在Point2d的對象pt2d中,有三個東西,首先是繼承自基類pt對象的數(shù)據(jù)成員_x,然后是pt2d對象本身的數(shù)據(jù)成員_y,最后是_vptr_Point。其中_vptr_Point指向著virtual table point2d。由于Point2d繼承自Point,所以在virtual table point2d中存儲著:改寫了的其中的~Point2d()、Point2d& mult( float )、float y() const,以及未被改寫的Point::z()函數(shù)。

            class Point3d: public Point2d { 
            public: 
               Point3d( float x = 0.0, 
                        float y = 0.0, float z = 0.0 ) 
                  : Point2d( x, y ), _z( z ) {} 
               ~Point3d();

               // overridden base class virtual functions 
               Point3d& mult( float ); 
               float z() const { return _z; }

               // ... other operations ... 
            protected: 
               float _z; 
            };

                  3、在Point3d的對象pt3d中,則有四個東西,一個是_x,一個是_vptr_Point,一個是_y,一個是_z。其中_vptr_Point指向著virtual table point3d。由于point3d繼承自point2d,所以在virtual table point3d中存儲著:已經(jīng)改寫了的point3d的~Point3d(),point3d::mult()的函數(shù)地址,和z()函數(shù)的地址,以及未被改寫的point2d的y()函數(shù)地址。

            ok,上述1、2、3所有情況的詳情,請參考下圖。

            本文,日后可能會酌情考慮增補有關(guān)內(nèi)容。ok,更多,可參考深度探索c++對象模型一書第四章。
            最近幾章難度都比較小,是考慮到狂想曲有深有淺的原則,后續(xù)章節(jié)會逐步恢復(fù)到相應(yīng)難度。

             

            第四節(jié)、虛函數(shù)的布局與匯編層面的考察

                  ivan、老夢的兩篇文章繼續(xù)對虛函數(shù)進行了一番深入,我看他們已經(jīng)寫得很好了,我就不饒舌了。ok,請看:1、VC虛函數(shù)布局引發(fā)的問題,2、從匯編層面深度剖析C++虛函數(shù)、http://blog.csdn.net/linyt/archive/2011/04/20/6336762.aspx。

             

            第五節(jié)、虛函數(shù)表的詳解

                本節(jié)全部內(nèi)容來自淄博的共享,非常感謝。注@molixiaogemao:只有發(fā)生繼承的時候且父類子類都有virtual的時候才會出現(xiàn)虛函數(shù)指針,請不要忘了虛函數(shù)出現(xiàn)的目的是為了實現(xiàn)多態(tài)。
             

             一般繼承(無虛函數(shù)覆蓋)
             下面,再讓我們來看看繼承時的虛函數(shù)表是什么樣的。假設(shè)有如下所示的一個繼承關(guān)系:

            請注意,在這個繼承關(guān)系中,子類沒有重載任何父類的函數(shù)。那么,在派生類的實例中,

             對于實例:Derive d; 的虛函數(shù)表如下:


            我們從表中可以看到下面幾點,
             1)覆蓋的f()函數(shù)被放到了虛表中原來父類虛函數(shù)的位置。
             2)沒有被覆蓋的函數(shù)依舊。
             
             這樣,我們就可以看到對于下面這樣的程序,
             Base *b = new Derive();

            b->f();

            由b所指的內(nèi)存中的虛函數(shù)表的f()的位置已經(jīng)被Derive::f()函數(shù)地址所取代,
            于是在實際調(diào)用發(fā)生時,是Derive::f()被調(diào)用了。這就實現(xiàn)了多態(tài)。


            多重繼承(無虛函數(shù)覆蓋)

            下面,再讓我們來看看多重繼承中的情況,假設(shè)有下面這樣一個類的繼承關(guān)系(注意:子類并沒有覆蓋父類的函數(shù)):


            我們可以看到:
            1) 每個父類都有自己的虛表。
            2) 子類的成員函數(shù)被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

            這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調(diào)用到實際的函數(shù)。


            多重繼承(有虛函數(shù)覆蓋)
            下面我們再來看看,如果發(fā)生虛函數(shù)覆蓋的情況。
            下圖中,我們在子類中覆蓋了父類的f()函數(shù)。


            我們可以看見,三個父類虛函數(shù)表中的f()的位置被替換成了子類的函數(shù)指針。
            這樣,我們就可以任一靜態(tài)類型的父類來指向子類,并調(diào)用子類的f()了。如:

            Derive d;
            Base1 *b1 = &d;
            Base2 *b2 = &d;
            Base3 *b3 = &d;
            b1->f(); //Derive::f()
            b2->f(); //Derive::f()
            b3->f(); //Derive::f()
            b1->g(); //Base1::g()
            b2->g(); //Base2::g()
            b3->g(); //Base3::g()

             

            安全性
            每次寫C++的文章,總免不了要批判一下C++。
            這篇文章也不例外。通過上面的講述,相信我們對虛函數(shù)表有一個比較細致的了解了。
            水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛函數(shù)表來干點什么壞事吧。

            一、通過父類型的指針訪問子類自己的虛函數(shù)
            我們知道,子類沒有重載父類的虛函數(shù)是一件毫無意義的事情。因為多態(tài)也是要基于函數(shù)重載的。
            雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數(shù),但我們根本不可能使用下面的語句來調(diào)用子類的自有虛函數(shù):

            Base1 *b1 = new Derive();
            b1->g1(); //編譯出錯

            任何妄圖使用父類指針想調(diào)用子類中的未覆蓋父類的成員函數(shù)的行為都會被編譯器視為非法,即基類指針不能調(diào)用子類自己定義的成員函數(shù)。所以,這樣的程序根本無法編譯通過。
            但在運行時,我們可以通過指針的方式訪問虛函數(shù)表來達到違反C++語義的行為。
            (關(guān)于這方面的嘗試,通過閱讀后面附錄的代碼,相信你可以做到這一點)

            二、訪問non-public的虛函數(shù)
            另外,如果父類的虛函數(shù)是private或是protected的,但這些非public的虛函數(shù)同樣會存在于虛函數(shù)表中,
            所以,我們同樣可以使用訪問虛函數(shù)表的方式來訪問這些non-public的虛函數(shù),這是很容易做到的。
            如:

            class Base {
            private: 
             virtual void f() { cout << "Base::f" << endl; } 
            };

            class Derive : public Base{ 
            };
            typedef void(*Fun)(void);
            void main() {
             Derive d;
             Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
             pFun(); 
            }

            對上面粗體部分的解釋(@a && x):

            1. (int*)(&d)取vptr地址,該地址存儲的是指向vtbl的指針
            2. (int*)*(int*)(&d)取vtbl地址,該地址存儲的是虛函數(shù)表數(shù)組
            3. (Fun)*((int*)*(int*)(&d) +0),取vtbl數(shù)組的第一個元素,即Base中第一個虛函數(shù)f的地址
            4. (Fun)*((int*)*(int*)(&d) +1),取vtbl數(shù)組的第二個元素(這第4點,如下圖所示)。

            下圖也能很清晰的說明一些東西(@5):


            ok,再來看一個問題,如果一個子類重載的虛擬函數(shù)為privete,那么通過父類的指針可以訪問到它嗎?

            #include <IOSTREAM>   
            class B   
            {    
            public:    
                virtual void fun()      
                {     
                    std::cout << "base fun called";     
                };    
            };  

            class D : public B    
            {    
            private:   
                virtual void fun()      
                {     
                    std::cout << "driver fun called";    
                };    
            };  

            int main(int argc, char* argv[])   
            {       
                B* p = new D();    
                p->fun();    
                return 0;    
            }  

            運行時會輸出 driver fun called

            從這個實驗,可以更深入的了解虛擬函數(shù)編譯時的一些特征:
            在編譯虛擬函數(shù)調(diào)用的時候,例如p->fun(); 只是按其靜態(tài)類型來處理的, 在這里p的類型就是B,不會考慮其實際指向的類型(動態(tài)類型)。

                也就是說,碰到p->fun();編譯器就當(dāng)作調(diào)用B的fun來進行相應(yīng)的檢查和處理。
            因為在B里fun是public的,所以這里在“訪問控制檢查”這一關(guān)就完全可以通過了。
            然后就會轉(zhuǎn)換成(*p->vptr[1])(p)這樣的方式處理, p實際指向的動態(tài)類型是D,
                所以p作為參數(shù)傳給fun后(類的非靜態(tài)成員函數(shù)都會編譯加一個指針參數(shù),指向調(diào)用該函數(shù)的對象,我們平常用的this就是該指針的值), 實際運行時p->vptr[1]則獲取到的是D::fun()的地址,也就調(diào)用了該函數(shù), 這也就是動態(tài)運行的機理。


            為了進一步的實驗,可以將B里的fun改為private的,D里的改為public的,則編譯就會出錯。
            C++的注意條款中有一條" 絕不重新定義繼承而來的缺省參數(shù)值" 
            (Effective C++ Item37, never redefine a function's inherited default parameter value) 也是同樣的道理。

            可以再做個實驗
            class B   
            {    
            public:   
                virtual void fun(int i = 1)      
                {     
                    std::cout << "base fun called, " << i;     
                };    
            };  

            class D : public B    
            {    
            private:    
                virtual void fun(int i = 2)      
                {     
                    std::cout << "driver fun called, " << i;     
                };    
            }; 

             

             

            則運行會輸出driver fun called, 1

             

            關(guān)于這一點,Effective上講的很清楚“virtual 函數(shù)系動態(tài)綁定, 而缺省參數(shù)卻是靜態(tài)綁定”,
            也就是說在編譯的時候已經(jīng)按照p的靜態(tài)類型處理其默認參數(shù)了,轉(zhuǎn)換成了(*p->vptr[1])(p, 1)這樣的方式。
             

            補遺

               一個類如果有虛函數(shù),不管是幾個虛函數(shù),都會為這個類聲明一個虛函數(shù)表,這個虛表是一個含有虛函數(shù)的類的,不是說是類對象的。一個含有虛函數(shù)的類,不管有多少個數(shù)據(jù)成員,每個對象實例都有一個虛指針,在內(nèi)存中,存放每個類對象的內(nèi)存區(qū),在內(nèi)存區(qū)的頭部都是先存放這個指針變量的(準確的說,應(yīng)該是:視編譯器具體情況而定),從第n(n視實際情況而定)個字節(jié)才是這個對象自己的東西。

             

            下面再說下通過基類指針,調(diào)用虛函數(shù)所發(fā)生的一切:
            One *p;
            p->disp();

            1、上來要取得類的虛表的指針,就是要得到,虛表的地址。存放類對象的內(nèi)存區(qū)的前四個字節(jié)其實就是用來存放虛表的地址的。
            2、得到虛表的地址后,從虛表那知道你調(diào)用的那個函數(shù)的入口地址。根據(jù)虛表提供的你要找的函數(shù)的地址。并調(diào)用函數(shù);你要知道,那個虛表是一個存放指針變量的數(shù)組,并不是說,那個虛表中就是存放的虛函數(shù)的實體。

            本章完。



            伊人久久大香线蕉综合网站| 日韩精品国产自在久久现线拍| 久久久WWW成人| 久久久久亚洲精品男人的天堂| 亚洲国产精品成人AV无码久久综合影院 | 中文字幕无码久久精品青草| 一本久久a久久精品vr综合| 久久精品国产亚洲AV无码娇色| 国产精品久久久久aaaa| 国产精品内射久久久久欢欢| 一97日本道伊人久久综合影院| 精品国产乱码久久久久久呢| 国产精品久久亚洲不卡动漫| 久久99精品久久久久久秒播| 97精品伊人久久大香线蕉| 久久99精品国产麻豆宅宅| 色诱久久av| 亚洲狠狠综合久久| 亚洲国产一成人久久精品| 精品久久人人爽天天玩人人妻| 青青草原综合久久大伊人| 久久午夜电影网| 香蕉久久夜色精品升级完成| 国产免费久久精品99久久| 国产∨亚洲V天堂无码久久久| 伊人 久久 精品| 亚洲国产二区三区久久| 午夜人妻久久久久久久久| 欧美精品丝袜久久久中文字幕 | 久久精品无码专区免费东京热| 狠狠人妻久久久久久综合蜜桃| 精品国产VA久久久久久久冰| 久久天天躁狠狠躁夜夜avapp | 久久久久久国产a免费观看黄色大片| 伊人久久综在合线亚洲2019 | 国内精品伊人久久久久av一坑| 欧美粉嫩小泬久久久久久久| 国产精品无码久久综合网| 狠狠色婷婷综合天天久久丁香 | 久久无码一区二区三区少妇| 久久国产影院|