C++虛擬函式

清風颺發表於2016-05-20

http://blog.sina.com.cn/s/blog_625ef6610101g9f2.html


定義:在某基類中宣告為 virtual 並在一個或多個派生類中被重新定 義的成員函式
語法:virtual 函式返回型別 函式名(參數列) {函式體;}
用途實現多型性,通過指向派生類基類指標,訪問派生類中同名覆蓋成員函式
虛擬函式必須是基類的非靜態成員函式,其訪問許可權可以是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示例

例項

#include
using namespace std;
class Cshape{
public:
void SetColor(int color) { m_nColor=color;}
virtual void Display(void) {cout<<"Cshape"<<endl; }
private:
int m_nColor;
};
class Crectangle:public Cshape{
public:
virtual void Display(void) {cout<<"Crectangle"<<endl; }
};
class Ctriangle:public Cshape {
virtual void Display(void) {cout<<"Ctriangle"<<endl; }
};
class Cellipse :public Cshape {
public:
virtual void Display(void) {cout<<"Cellipse"<<endl ;}
};
void main(){
Cshape obShape;
Cellipse obEllipse;
Ctriangle obTriangle;
Crectangle obRectangle;
Cshape * pShape[4]= { &obShape,&obEllipse,&obTriangle,& obRectangle };
for(int i= 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),多型性是將介面與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而採用不同的策略。下面來看一段簡單的程式碼
class A{
public:
void print(){ cout<<"This is A"<<endl;}
};
class B:public A{
public:
void print(){ cout<<"This is B"<<endl;}
};
int main(){ //為了在以後便於區分,我這段main()程式碼叫做main1
A a;
B b;
a.print();
b.print();
}
通過class A和class B的print()這個介面,可以看出這兩個class因個體的差異而採用了不同的策略,輸出的結果也是我們預料中的,分別是This is A和This is B。但這是否真正做到了多型性呢?No,多型還有個關鍵之處就是一切用指向基類指標或引用來操作物件。那現在就把main()處的程式碼改一改。
int main(){ //main2
A a;
B b;
A* p1=&a;
A* p2=&b;
p1->print();
p2->print();
}
執行一下看看結果,喲呵,驀然回首,結果卻是兩個This is A。問題來了,p2明明指向的是class B的物件但卻是呼叫的class A的print()函式,這不是我們所期望的結果,那麼解決這個問題就需要用到虛擬函式
class A{
public:
virtual void print(){ cout<<"This is A"<<endl;} //現在成了虛擬函式了
};
class B:public A{
public:
void print(){ cout<<"This is B"<<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》這本書,但又急切想知道,那你就應該從這裡開始)
虛擬函式是如何做到因物件的不同而呼叫其相應的函式的呢?現在我們就來剖析虛擬函式。我們先定義兩個類
class A{ //虛擬函式示例程式碼
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;}
};
由於這兩個類中有虛擬函式存在,所以編譯器就會為他們兩個分別插入一段你不知道的資料,併為他們分別建立一個表。那段資料叫做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指標。通過上面的分析,現在我們來實作一段程式碼,來描述這個帶有虛擬函式的類的簡單模型。
#include
using namespace std;
//將上面“虛擬函式示例程式碼”新增在這裡
int main(){
void (*fun)(A*);
A *p=new B;
long lVptrAddr;
memcpy(&lVptrAddr,p,4);
memcpy(&fun,reinterpret_cast(lVptrAddr),4);
fun(p);
delete p;
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(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(lVptrAddr+4),4); 為什麼是加4呢?因為一個指標的長度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast(lVptrAddr)+1,4); 這更符合陣列的用法,因為lVptrAddr被轉成了long*型別,所以+1就是往後移sizeof(long)的長度

三, 程式碼示例

#include
using namespace std;
class A{ //虛擬函式示例程式碼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//虛擬函式示例程式碼2
int main(){
void (A::*fun)(); //定義一個函式指標
A *p=new B;
fun=&A::fun;
(p->*fun)();
fun = &A::fun2;
(p->*fun)();
delete p;
system("pause");
}
你能估算出輸出結果嗎?如果你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是B::fun和B::fun2,如果你想不通就接著往下看。給個提示,&A::fun和&A::fun2是真正獲得了虛擬函式的地址嗎?
首先我們回到第二部分,通過段實作程式碼,得到一個“通用”的獲得虛擬函式地址的方法
#include
using namespace std;
//將上面“虛擬函式示例程式碼2”新增在這裡
void CallVirtualFun(void* pThis,int index=0){
void (*funptr)(void*);
long lVptrAddr;
memcpy(&lVptrAddr,pThis,4);
memcpy(&funptr,reinterpret_cast(lVptrAddr)+index,4);
funptr(pThis); //呼叫
}
int main(){
A* p=new B;
CallVirtualFun(p); //呼叫虛擬函式p->fun()
CallVirtualFun(p,1);//呼叫虛擬函式p->fun2()
system("pause");
}

CallVirtualFun

現在我們擁有一個“通用”的CallVirtualFun方法。
這個通用方法和第三部分開始處的程式碼有何聯絡呢?聯絡很大。由於A::fun()和A::fun2()是虛擬函式,所以&A::fun和&A::fun2獲得的不是函式的地址,而是一段間接獲得虛擬函式地址的一段程式碼的地址,我們形象地把這段程式碼看作那段CallVirtualFun。編譯器在編譯時,會提供類似於CallVirtualFun這樣的程式碼,當你呼叫虛擬函式時,其實就是先呼叫的那段類似CallVirtualFun的程式碼,通過這段程式碼,獲得虛擬函式地址後,最後呼叫虛擬函式,這樣就真正保證了多型性。同時大家都說虛擬函式的效率低,其原因就是,在呼叫虛擬函式之前,還呼叫了獲得虛擬函式地址的程式碼。

5其他資訊

定義虛擬函式的限制:(1)非類的成員函式不能定義為虛擬函式,類的成員函式中靜態成員函式和建構函式也不能定義為虛擬函式,但可以將解構函式定義為虛擬函式。實際上,優秀的程式設計師常常把基類解構函式定義為虛擬函式。因為,將基類解構函式定義為虛擬函式後,當利用delete刪除一個指向派生類定義的物件指標時,系統會呼叫相應的類的解構函式。而不將解構函式定義為虛擬函式時,只呼叫基類的解構函式。
(2)只需要在宣告函式的類體中使用關鍵字“virtual”將函式宣告為虛擬函式,而定義函式時不需要使用關鍵字“virtual”。
(3)當將基類中的某一成員函式宣告為虛擬函式後,派生類中的同名函式自動成為虛擬函式。
(4)如果宣告瞭某個成員函式為虛擬函式,則在該類中不能出現和這個成員函式同名並且返回值、引數個數、型別都相同的非虛擬函式。在以該類為基類派生類中,也不能出現這種同名函式。
虛擬函式聯絡到多型,多型聯絡到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。

6最後說明

本文的程式碼可以用VC6和Dev-C++4.9.8.0通過編譯,且執行無問題。其他的編譯器小弟不敢保證。其中的類比方法只能看成模型,因為不同的編譯器的底層實現是不同的。例如this指標,Dev-C++的gcc就是通過壓棧,當作引數傳遞,而VC的編譯器則通過取出地址儲存在ecx中。所以這些類比方法不能當作具體實現。

#include <iostream>
using namespace::std;

//普通函式的重寫
class father
{
private:
    int a;
public://加 virtual 是虛擬函式
   virtual void jump(void)
    {
    cout<<"jump 1.3M";
    };
    
    void sing(void)
    {
        cout<<"sing";
    }
    virtual void hehhe(void)
    {
    cout<<"hehe";
    }
    //有virtul加8個位元組
};

class son:public father
{
public:
    void jump(void)
    {
    cout<<"jump 2m";
    };

};

//虛擬函式的重寫
//虛擬函式-表 virtul table


int main(int argc, const char * argv[]) {
    // insert code here...
    std::cout << "Hello, World!\n";
    
    father f;
    f.jump();
    son s;
    s.jump();
    
    father * fp=new son;
    fp->jump();//列印父類的方法
    //編譯器決定call誰,編譯器只認型別,對比OC oc所有方法都是虛方法
    
    father &fr=s;
    fr.jump();
    
    //如果父類的方法加了virtul 關鍵字,就呼叫了子類的方法
    return 0;
}



相關文章