生活随笔
收集整理的這篇文章主要介紹了
虚函数编辑
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
收藏 1643 272
虛函數(shù) 編輯
本詞條缺少
名片圖 ,補(bǔ)充相關(guān)內(nèi)容使詞條更完整,還能快速升級(jí),趕緊來編輯吧!
在某基類中聲明為 virtual 并在一個(gè)或多個(gè)派生類中被重新定 義的成員函數(shù),virtual 函數(shù)返回類型 函數(shù)名(參數(shù)表) {函數(shù)體;},實(shí)現(xiàn)多態(tài)性,通過指向派生類的基類指針或引用,訪問派生類中同名覆蓋成員函數(shù)
中文名 虛函數(shù)
定????義 被virtual關(guān)鍵字修飾的成員函數(shù)
作????用 實(shí)現(xiàn)多態(tài)性
形象解釋 求同存異
關(guān)????鍵 用指向基類的指針或引用操作對(duì)象
聲????明 virtual?
目錄 1 c++的
? 一,定義 ? 二, 實(shí)現(xiàn) ? 三, 代碼示例 ? CallVirtualFun
1 c++的 編輯 下面是對(duì)C++的虛函數(shù)的理解。
一,定義 簡(jiǎn)單地說,那些被virtual關(guān)鍵字修飾的成員函數(shù),就是虛函數(shù)。虛函數(shù)的作用,用專業(yè)術(shù)語來解釋就是實(shí)現(xiàn)多態(tài)性(Polymorphism),多態(tài)性是將接口與實(shí)現(xiàn)進(jìn)行分離;用形象的語言來解釋就是實(shí)現(xiàn)以共同的方法,但因個(gè)體差異而采用不同的策略。下面來看一段簡(jiǎn)單的代碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include<iostream> usingnamespacestd; classA { public: voidprint() { cout<<"ThisisA"<<endl; } }; classB:publicA { public: voidprint() { cout<<"ThisisB"<<endl; } }; intmain() { //為了在以后便于區(qū)分,我這段main()代碼叫做main1 Aa; Bb; a.print(); b.print(); return0; }
輸出結(jié)果分別是“ThisisA”、“ThisisB”。通過class A和class B的print()這個(gè)接口,可以看出這兩個(gè)class因個(gè)體的差異而采用了不同的策略,但這是否真正做到了多態(tài)性呢?No,多態(tài)還有個(gè)關(guān)鍵之處就是一切用指向基類的指針或引用來操作對(duì)象。那現(xiàn)在就把main()處的代碼改一改。
1 2 3 4 5 6 7 8 9 10 11 intmain() { //main2 Aa; Bb; A*p1=&a; A*p2=&b; p1->print(); p2->print(); return0; }
運(yùn)行一下看看結(jié)果,喲呵,驀然回首,結(jié)果卻是兩個(gè)This is A。問題來了,p2明明指向的是class B的對(duì)象但卻是調(diào)用的class A的print()函數(shù),這不是我們所期望的結(jié)果,那么解決這個(gè)問題就需要用到虛函數(shù)
1 2 3 4 5 6 7 8 9 10 11 classA { public: virtualvoidprint(){cout<<"ThisisA"<<endl;} }; classB:publicA { public: voidprint(){cout<<"ThisisB"<<endl;} };
毫無疑問,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)鍵字修飾,那就是你自己的問題了。 現(xiàn)在重新運(yùn)行main2的代碼,這樣輸出的結(jié)果就是This is A和This is B了。 現(xiàn)在來消化一下,我作個(gè)簡(jiǎn)單的總結(jié),指向基類的指針在操作它的多態(tài)類對(duì)象時(shí),會(huì)根據(jù)不同的類對(duì)象,調(diào)用其相應(yīng)的函數(shù),這個(gè)函數(shù)就是虛函數(shù)。
二, 實(shí)現(xiàn) (如果你沒有看過《Inside The C++ Object Model》這本書,但又急切想知道,那你就應(yīng)該從這里開始) 虛函數(shù)是如何做到因?qū)ο蟮牟煌{(diào)用其相應(yīng)的函數(shù)的呢?現(xiàn)在我們就來剖析虛函數(shù)。我們先定義兩個(gè)類
1 2 3 4 5 6 7 8 9 10 classA{//虛函數(shù)示例代碼 public: virtualvoidfun(){cout<<1<<endl;} virtualvoidfun2(){cout<<2<<endl;} }; classB:publicA{ public: voidfun(){cout<<3<<endl;} voidfun2(){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)看圖
通過左圖,可以看到這兩個(gè)vtbl分別為class A和class B服務(wù)。現(xiàn)在有了這個(gè)模型之后,我們來分析下面的代碼 A *p=new A; p->fun(); 毫無疑問,調(diào)用了A::fun(),但是A::fun()是如何被調(diào)用的呢?它像普通函數(shù)那樣直接跳轉(zhuǎn)到函數(shù)的代碼處嗎?No,其實(shí)是這樣的,首先是取出vptr的值,這個(gè)值就是vtbl的地址,再根據(jù)這個(gè)值來到vtbl這里,由于調(diào)用的函數(shù)A::fun()是第一個(gè)虛函數(shù),所以取出vtbl第一個(gè)slot里的值,這個(gè)值就是A::fun()的地址了,最后調(diào)用這個(gè)函數(shù)。現(xiàn)在我們可以看出來了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里裝著對(duì)應(yīng)類的虛函數(shù)地址,所以這樣虛函數(shù)就可以完成它的任務(wù)。 而對(duì)于class A和class B來說,他們的vptr指針存放在何處呢?其實(shí)這個(gè)指針就放在他們各自的實(shí)例對(duì)象里。由于class A和class B都沒有數(shù)據(jù)成員,所以他們的實(shí)例對(duì)象里就只有一個(gè)vptr指針。通過上面的分析,現(xiàn)在我們來實(shí)作一段代碼,來描述這個(gè)帶有虛函數(shù)的類的簡(jiǎn)單模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include<iostream> usingnamespacestd; //將上面“虛函數(shù)示例代碼”添加在這里 intmain(){ void(*fun)(A*); A*p=newB; longlVptrAddr; memcpy(&lVptrAddr,p,4); memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); fun(p); deletep; system("pause"); return0; }
用VC或Dev-C++編譯運(yùn)行一下,看看結(jié)果是不是輸出3,如果不是,那么太陽明天肯定是從西邊出來。現(xiàn)在一步一步開始分析 void (*fun)(A*); 這段定義了一個(gè)函數(shù)指針名字叫做fun,而且有一個(gè)A*類型的參數(shù),這個(gè)函數(shù)指針待會(huì)兒用來保存從vtbl里取出的函數(shù)地址 A* p=new B; new B是向內(nèi)存(內(nèi)存分5個(gè)區(qū):全局名字空間,自由存儲(chǔ)區(qū),寄存器,代碼空間,棧)自由存儲(chǔ)區(qū)申請(qǐng)一個(gè)內(nèi)存單元的地址然后隱式地保存在一個(gè)指針中.然后把這個(gè)地址賦值給A類型的指針P. . long lVptrAddr; 這個(gè)long類型的變量待會(huì)兒用來保存vptr的值 memcpy(&lVptrAddr,p,4); 前面說了,他們的實(shí)例對(duì)象里只有vptr指針,所以我們就放心大膽地把p所指的4bytes內(nèi)存里的東西復(fù)制到lVptrAddr中,所以復(fù)制出來的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; 釋放由p指向的自由空間; system("pause"); 屏幕暫停; 如果調(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)度
三, 代碼示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include<iostream> usingnamespacestd; classA{//虛函數(shù)示例代碼2 public: virtualvoidfun(){cout<<"A::fun"<<endl;} virtualvoidfun2(){cout<<"A::fun2"<<endl;} }; classB:publicA{ public: voidfun(){cout<<"B::fun"<<endl;} voidfun2(){cout<<"B::fun2"<<endl;} };//end//虛函數(shù)示例代碼2 intmain(){ void(A::*fun)();//定義一個(gè)函數(shù)指針 A*p=newB; fun=&A::fun; (p->*fun)(); fun=&A::fun2; (p->*fun)(); deletep; system("pause"); return0; }
你能估算出輸出結(jié)果嗎?如果你估算出的結(jié)果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實(shí)真正的結(jié)果是B::fun和B::fun2,如果你想不通就接著往下看。給個(gè)提示,&A::fun和&A::fun2是真正獲得了虛函數(shù)的地址嗎? 首先我們回到第二部分,通過段實(shí)作代碼,得到一個(gè)“通用”的獲得虛函數(shù)地址的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include<iostream> usingnamespacestd; //將上面“虛函數(shù)示例代碼2”添加在這里 voidCallVirtualFun(void*pThis,intindex=0){ void(*funptr)(void*); longlVptrAddr; memcpy(&lVptrAddr,pThis,4); memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4); funptr(pThis);//調(diào)用 } intmain(){ A*p=newB; CallVirtualFun(p);//調(diào)用虛函數(shù)p->fun() CallVirtualFun(p,1);//調(diào)用虛函數(shù)p->fun2() system("pause"); return0; }
CallVirtualFun 現(xiàn)在我們擁有一個(gè)“通用”的CallVirtualFun方法。 這個(gè)通用方法和第三部分開始處的代碼有何聯(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的代碼,通過這段代碼,獲得虛函數(shù)地址后,最后調(diào)用虛函數(shù),這樣就真正保證了多態(tài)性。同時(shí)大家都說虛函數(shù)的效率低,其原因就是,在調(diào)用虛函數(shù)之前,還調(diào)用了獲得虛函數(shù)地址的代碼。 其他信息 定義虛函數(shù)的限制:(1)非類的成員函數(shù)不能定義為虛函數(shù),類的成員函數(shù)中靜態(tài)成員函數(shù)和構(gòu)造函數(shù)也不能定義為虛函數(shù),但可以將析構(gòu)函數(shù)定義為虛函數(shù)。實(shí)際上,優(yōu)秀的程序員常常把基類的析構(gòu)函數(shù)定義為虛函數(shù)。因?yàn)?#xff0c;將基類的析構(gòu)函數(shù)定義為虛函數(shù)后,當(dāng)利用delete刪除一個(gè)指向派生類定義的對(duì)象指針時(shí),系統(tǒng)會(huì)調(diào)用相應(yīng)的類的析構(gòu)函數(shù)。而不將析構(gòu)函數(shù)定義為虛函數(shù)時(shí),只調(diào)用基類的析構(gòu)函數(shù)。 (2)只需要在聲明函數(shù)的類體中使用關(guān)鍵字“virtual”將函數(shù)聲明為虛函數(shù),而定義函數(shù)時(shí)不需要使用關(guān)鍵字“virtual”。 (3)當(dāng)將基類中的某一成員函數(shù)聲明為虛函數(shù)后,派生類中的同名函數(shù)自動(dòng)成為虛函數(shù)。 (4)如果聲明了某個(gè)成員函數(shù)為虛函數(shù),則在該類中不能出現(xiàn)和這個(gè)成員函數(shù)同名并且返回值、參數(shù)個(gè)數(shù)、類型都相同的非虛函數(shù)。在以該類為基類的派生類中,也不能出現(xiàn)這種同名函數(shù)。 虛函數(shù)聯(lián)系到多態(tài),多態(tài)聯(lián)系到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什么都沒得談。 最后說明 本文的代碼可以用VC6和Dev-C++4.9.8.0通過編譯,且運(yùn)行無問題。其他的編譯器小弟不敢保證。其中的類比方法只能看成模型,因?yàn)椴煌木幾g器的底層實(shí)現(xiàn)是不同的。例如this指針,Dev-C++的gcc就是通過壓棧,當(dāng)作參數(shù)傳遞,而VC的編譯器則通過取出地址保存在ecx中。所以這些類比方法不能當(dāng)作具體實(shí)現(xiàn)。
總結(jié)
以上是生活随笔 為你收集整理的虚函数编辑 的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔 推薦給好友。