• <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>

            開(kāi)源之路

            憶往昔, 項(xiàng)羽不過(guò)江. 江東好風(fēng)光! 今振臂一呼,率甲三千, 試問(wèn)天!
            posts - 86, comments - 55, trackbacks - 0, articles - 0
              C++博客 :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理

            虛函數(shù)

            Posted on 2006-07-18 15:54 江邊之鳥(niǎo) 閱讀(392) 評(píng)論(2)  編輯 收藏 引用
            虛函數(shù)聯(lián)系到多態(tài),多態(tài)聯(lián)系到繼承。所以本文中都是在繼承層次上做文章。沒(méi)了繼承,什么都沒(méi)得談。
            ?
            下面是小弟對(duì)C++的虛函數(shù)這玩意兒的理解。
            ?
            一,? 什么是虛函數(shù)(如果不知道虛函數(shù)為何物,但有急切的想知道,那你就應(yīng)該從這里開(kāi)始)
            ?
            簡(jiǎn)單地說(shuō),那些被virtual關(guān)鍵字修飾的成員函數(shù),就是虛函數(shù)。虛函數(shù)的作用,用專業(yè)術(shù)語(yǔ)來(lái)解釋就是實(shí)現(xiàn)多態(tài)性(Polymorphism),多態(tài)性是將接口與實(shí)現(xiàn)進(jìn)行分離;用形象的語(yǔ)言來(lái)解釋就是實(shí)現(xiàn)以共同的方法,但因個(gè)體差異而采用不同的策略。下面來(lái)看一段簡(jiǎn)單的代碼
            ?
            class A{
            ?
            public:
            ?
            ??? void print(){ cout<<”This is A”<<endl;}
            ?
            };
            ?
            class B:public A{
            ?
            public:
            ?
            ??? void print(){ cout<<”This is B”<<endl;}
            ?
            };
            ?
            int main(){?? //為了在以后便于區(qū)分,我這段main()代碼叫做main1
            ?
            ?? A a;
            ?
            ?? B b;
            ?
            ?? a.print();
            ?
            ?? b.print();
            ?
            }
            ?
            通過(guò)class A和class B的print()這個(gè)接口,可以看出這兩個(gè)class因個(gè)體的差異而采用了不同的策略,輸出的結(jié)果也是我們預(yù)料中的,分別是This is A和This is B。但這是否真正做到了多態(tài)性呢?No,多態(tài)還有個(gè)關(guān)鍵之處就是一切用指向基類的指針或引用來(lái)操作對(duì)象。那現(xiàn)在就把main()處的代碼改一改。
            ?
            int main(){?? //main2
            ?
            ??? A a;
            ?
            ??? B b;
            ?
            ??? A* p1=&a;
            ?
            ??? A* p2=&b;
            ?
            ??? p1->print();
            ?
            ??? p2->print();
            ?
            }
            ?
            運(yùn)行一下看看結(jié)果,喲呵,驀然回首,結(jié)果卻是兩個(gè)This is A。問(wèn)題來(lái)了,p2明明指向的是class B的對(duì)象但卻是調(diào)用的class A的print()函數(shù),這不是我們所期望的結(jié)果,那么解決這個(gè)問(wèn)題就需要用到虛函數(shù)
            ?
            class A{
            ?
            public:
            ?
            ??? virtual void print(){ cout<<”This is A”<<endl;}? //現(xiàn)在成了虛函數(shù)了
            ?
            };
            ?
            class B:public A{
            ?
            public:
            ?
            ??? void print(){ cout<<”This is B”<<endl;}? //這里需要在前面加上關(guān)鍵字virtual嗎?
            ?
            };
            ?
            毫無(wú)疑問(wèn),class A的成員函數(shù)print()已經(jīng)成了虛函數(shù),那么class B的print()成了虛函數(shù)了嗎?回答是Yes,我們只需在把基類的成員函數(shù)設(shè)為virtual,其派生類的相應(yīng)的函數(shù)也會(huì)自動(dòng)變?yōu)樘摵瘮?shù)。所以,class B的print()也成了虛函數(shù)。那么對(duì)于在派生類的相應(yīng)函數(shù)前是否需要用virtual關(guān)鍵字修飾,那就是你自己的問(wèn)題了。
            ?
            現(xiàn)在重新運(yùn)行main2的代碼,這樣輸出的結(jié)果就是This is A和This is B了。
            ?
            現(xiàn)在來(lái)消化一下,我作個(gè)簡(jiǎn)單的總結(jié),指向基類的指針在操作它的多態(tài)類對(duì)象時(shí),會(huì)根據(jù)不同的類對(duì)象,調(diào)用其相應(yīng)的函數(shù),這個(gè)函數(shù)就是虛函數(shù)。
            ?
            二,? 虛函數(shù)是如何做到的(如果你沒(méi)有看過(guò)《Inside The C++ Object Model》這本書(shū),但又急切想知道,那你就應(yīng)該從這里開(kāi)始)
            ?
            虛函數(shù)是如何做到因?qū)ο蟮牟煌{(diào)用其相應(yīng)的函數(shù)的呢?現(xiàn)在我們就來(lái)剖析虛函數(shù)。我們先定義兩個(gè)類
            ?
            class A{?? //虛函數(shù)示例代碼
            ?
            public:
            ?
            ?? virtual void fun(){cout<<1<<endl;}
            ?
            ?? virtual void fun2(){cout<<2<<endl;}
            ?
            };
            ?
            class B:public A{
            ?
            public:
            ?
            ?? void fun(){cout<<3<<endl;}
            ?
            ?? void fun2(){cout<<4<<endl;}
            ?
            };
            ?
            由于這兩個(gè)類中有虛函數(shù)存在,所以編譯器就會(huì)為他們兩個(gè)分別插入一段你不知道的數(shù)據(jù),并為他們分別創(chuàng)建一個(gè)表。那段數(shù)據(jù)叫做vptr指針,指向那個(gè)表。那個(gè)表叫做vtbl,每個(gè)類都有自己的vtbl,vtbl的作用就是保存自己類中虛函數(shù)的地址,我們可以把vtbl形象地看成一個(gè)數(shù)組,這個(gè)數(shù)組的每個(gè)元素存放的就是虛函數(shù)的地址,請(qǐng)看圖
            ?
            ?
            ?
            通過(guò)上圖,可以看到這兩個(gè)vtbl分別為class A和class B服務(wù)。現(xiàn)在有了這個(gè)模型之后,我們來(lái)分析下面的代碼
            ?
            A *p=new A;
            ?
            p->fun();
            ?
            毫無(wú)疑問(wèn),調(diào)用了A::fun(),但是A::fun()是如何被調(diào)用的呢?它像普通函數(shù)那樣直接跳轉(zhuǎn)到函數(shù)的代碼處嗎?No,其實(shí)是這樣的,首先是取出vptr的值,這個(gè)值就是vtbl的地址,再根據(jù)這個(gè)值來(lái)到vtbl這里,由于調(diào)用的函數(shù)A::fun()是第一個(gè)虛函數(shù),所以取出vtbl第一個(gè)slot里的值,這個(gè)值就是A::fun()的地址了,最后調(diào)用這個(gè)函數(shù)。現(xiàn)在我們可以看出來(lái)了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里裝著對(duì)應(yīng)類的虛函數(shù)地址,所以這樣虛函數(shù)就可以完成它的任務(wù)。
            ?
            而對(duì)于class A和class B來(lái)說(shuō),他們的vptr指針存放在何處呢?其實(shí)這個(gè)指針就放在他們各自的實(shí)例對(duì)象里。由于class A和class B都沒(méi)有數(shù)據(jù)成員,所以他們的實(shí)例對(duì)象里就只有一個(gè)vptr指針。通過(guò)上面的分析,現(xiàn)在我們來(lái)實(shí)作一段代碼,來(lái)描述這個(gè)帶有虛函數(shù)的類的簡(jiǎn)單模型。
            ?
            #include<iostream>
            ?
            using namespace std;
            ?
            //將上面“虛函數(shù)示例代碼”添加在這里
            ?
            int main(){
            ?
            ? void (*fun)(A*);
            ?
            ? A *p=new B;
            ?
            ? long lVptrAddr;
            ?
            ? memcpy(&lVptrAddr,p,4);
            ?
            ? memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);
            ?
            ? fun(p);
            ?
            ? delete p;
            ?
            ? system("pause");
            ?
            }
            ?
            用VC或Dev-C++編譯運(yùn)行一下,看看結(jié)果是不是輸出3,如果不是,那么太陽(yáng)明天肯定是從西邊出來(lái)。現(xiàn)在一步一步開(kāi)始分析
            ?
            void (*fun)(A*);? 這段定義了一個(gè)函數(shù)指針名字叫做fun,而且有一個(gè)A*類型的參數(shù),這個(gè)函數(shù)指針待會(huì)兒用來(lái)保存從vtbl里取出的函數(shù)地址
            ?
            A* p=new B;? 這個(gè)我不太了解,算了,不解釋這個(gè)了
            ?
            long lVptrAddr;? 這個(gè)long類型的變量待會(huì)兒用來(lái)保存vptr的值
            ?
            memcpy(&lVptrAddr,p,4);? 前面說(shuō)了,他們的實(shí)例對(duì)象里只有vptr指針,所以我們就放心大膽地把p所指的4bytes內(nèi)存里的東西復(fù)制到lVptrAddr中,所以復(fù)制出來(lái)的4bytes內(nèi)容就是vptr的值,即vtbl的地址
            ?
            現(xiàn)在有了vtbl的地址了,那么我們現(xiàn)在就取出vtbl第一個(gè)slot里的內(nèi)容
            ?
            memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);? 取出vtbl第一個(gè)slot里的內(nèi)容,并存放在函數(shù)指針fun里。需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指針,所以我們要把它先轉(zhuǎn)變成指針類型
            ?
            fun(p);? 這里就調(diào)用了剛才取出的函數(shù)地址里的函數(shù),也就是調(diào)用了B::fun()這個(gè)函數(shù),也許你發(fā)現(xiàn)了為什么會(huì)有參數(shù)p,其實(shí)類成員函數(shù)調(diào)用時(shí),會(huì)有個(gè)this指針,這個(gè)p就是那個(gè)this指針,只是在一般的調(diào)用中編譯器自動(dòng)幫你處理了而已,而在這里則需要自己處理。
            ?
            delete p;和system("pause");? 這個(gè)我不太了解,算了,不解釋這個(gè)了
            ?
            如果調(diào)用B::fun2()怎么辦?那就取出vtbl的第二個(gè)slot里的值就行了
            ?
            memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 為什么是加4呢?因?yàn)橐粋€(gè)指針的長(zhǎng)度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 這更符合數(shù)組的用法,因?yàn)閘VptrAddr被轉(zhuǎn)成了long*型別,所以+1就是往后移sizeof(long)的長(zhǎng)度
            ?
            三,? 以一段代碼開(kāi)始
            ?
            #include<iostream>
            ?
            using namespace std;
            ?
            class A{?? //虛函數(shù)示例代碼2
            ?
            public:
            ?
            ? virtual void fun(){ cout<<"A::fun"<<endl;}
            ?
            ? virtual void fun2(){cout<<"A::fun2"<<endl;}
            ?
            };
            ?
            class B:public A{
            ?
            public:
            ?
            ? void fun(){ cout<<"B::fun"<<endl;}
            ?
            ? void fun2(){ cout<<"B::fun2"<<endl;}
            ?
            };? //end//虛函數(shù)示例代碼2
            ?
            int main(){
            ?
            void (A::*fun)();? //定義一個(gè)函數(shù)指針
            ?
            A *p=new B;
            ?
            fun=&A::fun;
            ?
            (p->*fun)();
            ?
            fun = &A::fun2;
            ?
            (p->*fun)();
            ?
            delete p;
            ?
            system("pause");
            ?
            }
            ?
            你能估算出輸出結(jié)果嗎?如果你估算出的結(jié)果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實(shí)真正的結(jié)果是B::fun和B::fun2,如果你想不通就接著往下看。給個(gè)提示,&A::fun和&A::fun2是真正獲得了虛函數(shù)的地址嗎?
            ?
            首先我們回到第二部分,通過(guò)段實(shí)作代碼,得到一個(gè)“通用”的獲得虛函數(shù)地址的方法
            ?
            #include<iostream>
            ?
            using namespace std;
            ?
            //將上面“虛函數(shù)示例代碼2”添加在這里
            ?
            void CallVirtualFun(void* pThis,int index=0){
            ?
            ? void (*funptr)(void*);
            ?
            ? long lVptrAddr;
            ?
            ? memcpy(&lVptrAddr,pThis,4);
            ?
            ? memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);
            ?
            ? funptr(pThis); //調(diào)用
            ?
            }
            ?
            int main(){
            ?
            ?? A* p=new B;
            ?
            ?? CallVirtualFun(p);? //調(diào)用虛函數(shù)p->fun()
            ?
            ?? CallVirtualFun(p,1);//調(diào)用虛函數(shù)p->fun2()
            ?
            ?? system("pause");
            ?
            }
            ?
            現(xiàn)在我們擁有一個(gè)“通用”的CallVirtualFun方法。
            ?
            這個(gè)通用方法和第三部分開(kāi)始處的代碼有何聯(lián)系呢?聯(lián)系很大。由于A::fun()和A::fun2()是虛函數(shù),所以&A::fun和&A::fun2獲得的不是函數(shù)的地址,而是一段間接獲得虛函數(shù)地址的一段代碼的地址,我們形象地把這段代碼看作那段CallVirtualFun。編譯器在編譯時(shí),會(huì)提供類似于CallVirtualFun這樣的代碼,當(dāng)你調(diào)用虛函數(shù)時(shí),其實(shí)就是先調(diào)用的那段類似CallVirtualFun的代碼,通過(guò)這段代碼,獲得虛函數(shù)地址后,最后調(diào)用虛函數(shù),這樣就真正保證了多態(tài)性。同時(shí)大家都說(shuō)虛函數(shù)的效率低,其原因就是,在調(diào)用虛函數(shù)之前,還調(diào)用了獲得虛函數(shù)地址的代碼。
            ?
            ?

            最后的說(shuō)明:本文的代碼可以用VC6和Dev-C++4.9.8.0通過(guò)編譯,且運(yùn)行無(wú)問(wèn)題。其他的編譯器小弟不敢保證。其中,里面的類比方法只能看成模型,因?yàn)椴煌木幾g器的低層實(shí)現(xiàn)是不同的。例如this指針,Dev-C++的gcc就是通過(guò)壓棧,當(dāng)作參數(shù)傳遞,而VC的編譯器則通過(guò)取出地址保存在ecx中。所以這些類比方法不能當(dāng)作具體實(shí)現(xiàn)。

            Feedback

            # re: 虛函數(shù)[未登錄](méi)  回復(fù)  更多評(píng)論   

            2007-05-02 08:27 by Ben
            你的工作是干嗎的?

            # re: 虛函數(shù)  回復(fù)  更多評(píng)論   

            2007-05-09 10:21 by 江邊之鳥(niǎo)
            普通的程序員@Ben

            只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問(wèn)   Chat2DB   管理


            久久久久久亚洲Av无码精品专口| 亚洲AV日韩AV天堂久久| 久久天天躁狠狠躁夜夜2020老熟妇 | 久久露脸国产精品| 精品国产99久久久久久麻豆| 国内精品伊人久久久久av一坑| 精品欧美一区二区三区久久久| 久久精品国产亚洲av麻豆蜜芽 | 国产精品九九久久免费视频| 伊人情人综合成人久久网小说| 久久精品国产精品亚洲毛片| 亚洲国产成人久久精品99| 国产欧美久久一区二区| 怡红院日本一道日本久久 | 久久久精品人妻一区二区三区蜜桃| 亚洲狠狠综合久久| 久久精品99久久香蕉国产色戒| 久久这里只有精品视频99| 91精品国产9l久久久久| 久久妇女高潮几次MBA| 久久久久久久综合日本| 久久久久久久尹人综合网亚洲| 久久精品国产精品亚洲精品 | 久久九九精品99国产精品| 无码精品久久一区二区三区| 久久香蕉国产线看观看99 | 亚洲精品乱码久久久久久蜜桃| 精品999久久久久久中文字幕 | 99久久人人爽亚洲精品美女| 欧美噜噜久久久XXX| 精品人妻伦九区久久AAA片69| 亚洲国产成人精品91久久久 | 精品久久久久中文字幕一区| 97久久香蕉国产线看观看| 少妇内射兰兰久久| 亚洲伊人久久精品影院| 精品久久久久久中文字幕大豆网| 久久婷婷是五月综合色狠狠| 亚洲精品高清一二区久久| 久久精品免费网站网| 欧美久久一级内射wwwwww.|