虛函數

http://baike.baidu.com/link?url=heYKUVZNDYa2dR2slQccCLhDvoQ47KQQkmibQpOUPaFlKnHTwO6NeWxBBd_H4EEP


在某基類中聲明爲 virtual 並在一個或多個派生類中被重新定 義的成員函數,virtual 函數返回類型 函數名(參數表) {函數體;},實現多態性,通過指向派生類基類指針或引用,訪問派生類中同名覆蓋成員函數

1定義編輯

虛函數必須是基類的非靜態成員函數,其訪問權限可以是private或protected或public,在基類的類定義中定義虛函數的一般形式:
class基類名{
.......
virtual 返回值類型 將要在派生類重載的函數名(參數列表);
};

2作用編輯

虛函數的作用是實現動態聯編,也就是在程序的運行階段動態地選擇合適的成員函數,在定義了虛函數後,可以在基類派生類中對虛函數重新定義,在派生類中重新定義的函數應與虛函數具有相同的形參個數和形參類型。以實現統一的接口,不同定義過程。如果在派生類中沒有對虛函數重新定義,則它繼承其基類的虛函數。
當程序發現虛函數名前的關鍵字virtual後,會自動將其作爲動態聯編處理,即在程序運行時動態地選擇合適的成員函數。虛函數是C++多態的一種表現。
例如:子類繼承了父類的一個函數(方法),而我們把父類的指針指向子類,則必須把父類的該函數(方法)設爲virtual(虛函數)。
([2010.10.28] 注:下行語義容易使人產生理解上的偏差,實際效果應爲:
如存在:Base -> Derive1 -> Derive2 及它們所擁有的虛函數func()
則在訪問派生類Derive1的實例時,使用其基類Base及本身類型Derive1,或被靜態轉換的後續派生類Derive2的指針或引用,均可訪問到Derive1所實現的func()。)
動態聯編規定,只能通過指向基類指針或基類對象的引用來調用虛函數,其格式:
1、指向基類的指針變量名->虛函數名(實參表)
2、基類對象的引用名. 虛函數名(實參表)
使用虛函數,我們可以靈活的進行動態綁定,當然是以一定的開銷爲代價。如果父類的函數(方法)根本沒有必要或者無法實現,完全要依賴子類去實現的話,可以把此函數(方法)設爲virtual 函數名=0 我們把這樣的函數(方法)稱爲純虛函數
如果一個類包含了純虛函數,稱此類爲抽象類

3示例編輯

實例

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
30
31
32
33
34
35
#include<iostream>
usingnamespacestd;
classCshape
{
public:
voidSetColor(intcolor){m_nColor=color;}
virtualvoidDisplay(void){cout<<"Cshape"<<endl;}
private:
intm_nColor;
};
classCrectangle:publicCshape{
public:
virtualvoidDisplay(void)
{
cout<<"Crectangle"<<endl;
}
};
classCtriangle:publicCshape{
virtualvoidDisplay(void)
{cout<<"Ctriangle"<<endl;}
};
classCellipse:publicCshape{
public:
virtualvoidDisplay(void)
{cout<<"Cellipse"<<endl;}
};
voidmain(){
CshapeobShape;
CellipseobEllipse;
CtriangleobTriangle;
CrectangleobRectangle;
Cshape*pShape[4]={&obShape,&obEllipse,&obTriangle,&obRectangle};
for(inti=0;i<4;i++)
pShape[i]->Display();
}
本程序運行結果:
Cshape
Cellipse
Ctriangle
Crectangle
如果把Cshape類裏面virtual void Display(void) 中的virtual去掉的話
運行結果就不一樣了:
Cshape
Cshape
Cshape
Cshape

條件

所以,從以上程序分析,實現動態聯編需要三個條件:
1、 必須把動態聯編的行爲定義爲類的虛函數。
2、 類之間存在子類型關係,一般表現爲一個類從另一個類公有派生而來。
3、 必須先使用基類指針指向子類型的對象,然後直接或者間接使用基類指針調用虛函數。

4c++的編輯

下面是對C++的虛函數的理解。

一,定義

簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而採用不同的策略。下面來看一段簡單的代碼
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
30
31
classA
{
 
public:
 
voidprint(){cout<<"ThisisA"<<endl;}
 
};
 
classB:publicA
{
 
public:
 
voidprint(){cout<<"ThisisB"<<endl;}
 
};
 
intmain()
{
//爲了在以後便於區分,我這段main()代碼叫做main1
 
Aa;
 
Bb;
 
a.print();
 
b.print();
 
}
通過class A和class B的print()這個接口,可以看出這兩個class因個體的差異而採用了不同的策略,但這是否真正做到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類指針或引用來操作對象。那現在就把main()處的代碼改一改。
1
2
3
4
5
6
7
8
intmain(){//main2
Aa;
Bb;
A*p1=&a;
A*p2=&b;
p1->print();
p2->print();
}
運行一下看看結果,喲呵,驀然回首,結果卻是兩個This is A。問題來了,p2明明指向的是class B的對象但卻是調用的class A的print()函數,這不是我們所期望的結果,那麼解決這個問題就需要用到虛函數
1
2
3
4
5
6
7
8
classA{
public:
virtualvoidprint(){cout<<"ThisisA"<<endl;}//現在成了虛函數了
};
classB:publicA{
public:
voidprint(){cout<<"ThisisB"<<endl;}//這裏需要在前面加上關鍵字virtual嗎?
};
毫無疑問,class A的成員函數print()已經成了虛函數,那麼class B的print()成了虛函數了嗎?回答是Yes,我們只需在把基類的成員函數設爲virtual,其派生類的相應的函數也會自動變爲虛函數。所以,class B的print()也成了虛函數。那麼對於在派生類的相應函數前是否需要用virtual關鍵字修飾,那就是你自己的問題了。
現在重新運行main2的代碼,這樣輸出的結果就是This is A和This is B了。
現在來消化一下,我作個簡單的總結,指向基類指針在操作它的多態類對象時,會根據不同的類對象,調用其相應的函數,這個函數就是虛函數。

二, 實現

(如果你沒有看過《Inside The C++ Object Model》這本書,但又急切想知道,那你就應該從這裏開始)
虛函數是如何做到因對象的不同而調用其相應的函數的呢?現在我們就來剖析虛函數。我們先定義兩個類
1
2
3
4
5
6
7
8
9
10
classA{//虛函數示例代碼
public:
virtualvoidfun(){cout<<1<<endl;}
virtualvoidfun2(){cout<<2<<endl;}
};
classB:publicA{
public:
voidfun(){cout<<3<<endl;}
voidfun2(){cout<<4<<endl;}
};
由於這兩個類中有虛函數存在,所以編譯器就會爲他們兩個分別插入一段你不知道的數據,併爲他們分別創建一個表。那段數據叫做vptr指針,指向那個表。那個表叫做vtbl,每個類都有自己的vtbl,vtbl的作用就是保存自己類中虛函數的地址,我們可以把vtbl形象地看成一個數組,這個數組的每個元素存放的就是虛函數的地址,請看圖
通過左圖,可以看到這兩個vtbl分別爲class A和class B服務。現在有了這個模型之後,我們來分析下面的代碼
A *p=new A;
p->fun();
毫無疑問,調用了A::fun(),但是A::fun()是如何被調用的呢?它像普通函數那樣直接跳轉到函數的代碼處嗎?No,其實是這樣的,首先是取出vptr的值,這個值就是vtbl的地址,再根據這個值來到vtbl這裏,由於調用的函數A::fun()是第一個虛函數,所以取出vtbl第一個slot裏的值,這個值就是A::fun()的地址了,最後調用這個函數。現在我們可以看出來了,只要vptr不同,指向的vtbl就不同,而不同的vtbl裏裝着對應類的虛函數地址,所以這樣虛函數就可以完成它的任務。
而對於class A和class B來說,他們的vptr指針存放在何處呢?其實這個指針就放在他們各自的實例對象裏。由於class A和class B都沒有數據成員,所以他們的實例對象裏就只有一個vptr指針。通過上面的分析,現在我們來實作一段代碼,來描述這個帶有虛函數的類的簡單模型。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
usingnamespacestd;
//將上面“虛函數示例代碼”添加在這裏
intmain(){
void(*fun)(A*);
A*p=newB;
longlVptrAddr;
memcpy(&lVptrAddr,p,4);
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);
fun(p);
deletep;
system("pause");
}
用VC或Dev-C++編譯運行一下,看看結果是不是輸出3,如果不是,那麼太陽明天肯定是從西邊出來。現在一步一步開始分析
void (*fun)(A*); 這段定義了一個函數指針名字叫做fun,而且有一個A*類型的參數,這個函數指針待會兒用來保存從vtbl裏取出的函數地址
A* p=new B; new B是向內存(內存分5個區:全局名字空間,自由存儲區,寄存器,代碼空間,棧)自由存儲區申請一個內存單元的地址然後隱式地保存在一個指針中.然後把這個地址賦值給A類型的指針P.
.
long lVptrAddr; 這個long類型的變量待會兒用來保存vptr的值
memcpy(&lVptrAddr,p,4); 前面說了,他們的實例對象裏只有vptr指針,所以我們就放心大膽地把p所指的4bytes內存裏的東西複製到lVptrAddr中,所以複製出來的4bytes內容就是vptr的值,即vtbl的地址
現在有了vtbl的地址了,那麼我們現在就取出vtbl第一個slot裏的內容
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); 取出vtbl第一個slot裏的內容,並存放在函數指針fun裏。需要注意的是lVptrAddr裏面是vtbl的地址,但lVptrAddr不是指針,所以我們要把它先轉變成指針類型
fun(p); 這裏就調用了剛纔取出的函數地址裏的函數,也就是調用了B::fun()這個函數,也許你發現了爲什麼會有參數p,其實類成員函數調用時,會有個this指針,這個p就是那個this指針,只是在一般的調用中編譯器自動幫你處理了而已,而在這裏則需要自己處理。
delete p; 釋放由p指向的自由空間;
system("pause"); 屏幕暫停;
如果調用B::fun2()怎麼辦?那就取出vtbl的第二個slot裏的值就行了
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 爲什麼是加4呢?因爲一個指針的長度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 這更符合數組的用法,因爲lVptrAddr被轉成了long*型別,所以+1就是往後移sizeof(long)的長度

三, 代碼示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
usingnamespacestd;
classA{//虛函數示例代碼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//虛函數示例代碼2
intmain(){
void(A::*fun)();//定義一個函數指針
A*p=newB;
fun=&A::fun;
(p->*fun)();
fun=&A::fun2;
(p->*fun)();
deletep;
system("pause");
}
你能估算出輸出結果嗎?如果你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是B::fun和B::fun2,如果你想不通就接着往下看。給個提示,&A::fun和&A::fun2是真正獲得了虛函數的地址嗎?
首先我們回到第二部分,通過段實作代碼,得到一個“通用”的獲得虛函數地址的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
usingnamespacestd;
//將上面“虛函數示例代碼2”添加在這裏
voidCallVirtualFun(void*pThis,intindex=0){
void(*funptr)(void*);
longlVptrAddr;
memcpy(&lVptrAddr,pThis,4);
memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);
funptr(pThis);//調用
}
intmain(){
A*p=newB;
CallVirtualFun(p);//調用虛函數p->fun()
CallVirtualFun(p,1);//調用虛函數p->fun2()
system("pause");
}

CallVirtualFun

現在我們擁有一個“通用”的CallVirtualFun方法。
這個通用方法和第三部分開始處的代碼有何聯繫呢?聯繫很大。由於A::fun()和A::fun2()是虛函數,所以&A::fun和&A::fun2獲得的不是函數的地址,而是一段間接獲得虛函數地址的一段代碼的地址,我們形象地把這段代碼看作那段CallVirtualFun。編譯器在編譯時,會提供類似於CallVirtualFun這樣的代碼,當你調用虛函數時,其實就是先調用的那段類似CallVirtualFun的代碼,通過這段代碼,獲得虛函數地址後,最後調用虛函數,這樣就真正保證了多態性。同時大家都說虛函數的效率低,其原因就是,在調用虛函數之前,還調用了獲得虛函數地址的代碼。
其他信息
定義虛函數的限制:(1)非類的成員函數不能定義爲虛函數,類的成員函數中靜態成員函數和構造函數也不能定義爲虛函數,但可以將析構函數定義爲虛函數。實際上,優秀的程序員常常把基類析構函數定義爲虛函數。因爲,將基類析構函數定義爲虛函數後,當利用delete刪除一個指向派生類定義的對象指針時,系統會調用相應的類的析構函數。而不將析構函數定義爲虛函數時,只調用基類的析構函數。
(2)只需要在聲明函數的類體中使用關鍵字“virtual”將函數聲明爲虛函數,而定義函數時不需要使用關鍵字“virtual”。
(3)當將基類中的某一成員函數聲明爲虛函數後,派生類中的同名函數自動成爲虛函數。
(4)如果聲明瞭某個成員函數爲虛函數,則在該類中不能出現和這個成員函數同名並且返回值、參數個數、類型都相同的非虛函數。在以該類爲基類派生類中,也不能出現這種同名函數。
虛函數聯繫到多態,多態聯繫到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。
最後說明
本文的代碼可以用VC6和Dev-C++4.9.8.0通過編譯,且運行無問題。其他的編譯器小弟不敢保證。其中的類比方法只能看成模型,因爲不同的編譯器的底層實現是不同的。例如this指針,Dev-C++的gcc就是通過壓棧,當作參數傳遞,而VC的編譯器則通過取出地址保存在ecx中。所以這些類比方法不能當作具體實現。

發佈了11 篇原創文章 · 獲贊 0 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章