1. 程式人生 > >C++中虛擬函式的作用是什麼?它應該怎麼用呢?

C++中虛擬函式的作用是什麼?它應該怎麼用呢?

虛擬函式聯絡到多型,多型聯絡到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。下面是對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<iostream>

using namespace std;

//將上面“虛擬函式示例程式碼”新增在這裡

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++編譯執行一下,看看結果是不是輸出3,如果不是,那麼太陽明天肯定是從西邊出來。現在一步一步開始分析

void (*fun)(A*); 這段定義了一個函式指標名字叫做fun,而且有一個A*型別的引數,這個函式指標待會兒用來儲存從vtbl裡取出的函式地址

A* p=new B; 這個我不太瞭解,算了,不解釋這個了

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;和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)的長度

三, 以一段程式碼開始

#include<iostream>

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

using namespace std;

//將上面“虛擬函式示例程式碼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); //呼叫

}

int main(){

A* p=new B;

CallVirtualFun(p); //呼叫虛擬函式p->fun()

CallVirtualFun(p,1);//呼叫虛擬函式p->fun2()

system("pause");

}

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

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

相關推薦

C++虛擬函式作用是什麼?應該怎麼

虛擬函式聯絡到多型,多型聯絡到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麼都沒得談。下面是對C++的虛擬函式這玩意兒的理解。 一, 什麼是虛擬函式 (如果不知道虛擬函式為何物,但有急切的想知道,那你就應該從這裡開始)簡單地說,那些被virtual關鍵字修飾的成員

c++虛擬函式的理解

虛擬函式的作用,事實上就是實現了多型性,就是實現以共同的方法,但因個體差異而採用不同的策略。下面有程式碼例項來描述: class A{ public: void print(){ cout<<”This is A”<<endl;} }; class B:publ

C++虛擬函式函式

解構函式為什麼要宣告為虛 函式??? 基類的解構函式需要宣告為虛擬函式:  當派生類物件經由一個基類指標被刪除,而該基類帶著一個non-virtual解構函式,實際執行時通常發生的是物件的派生類成員沒有被銷燬。這也就是區域性銷燬,會發生記憶體洩漏,所以我們通常將基類的解構函式需要宣告為

C++虛擬函式工作原理

C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。 所謂泛型技術,比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。 虛擬函式表(

C++虛擬函式工作原理和 虛 繼承類的記憶體佔用大小計算

                      虛擬函式的實現要求物件攜帶額外的資訊,這些資訊用於在執行時確定該物件應該呼叫哪一個虛擬函式。典型情況下,這一資訊具有一種被稱為 vptr(virtual table pointer,虛擬函式表指標)的指標的形式。vptr 指向一個被稱為 vtbl(virtual t

c++虛擬函式和純虛擬函式定義

      只有用virtual宣告類的成員函式,使之成為虛擬函式,不能將類外的普通函式宣告為虛擬函式。因為虛擬函式的作用是允許在派生類中對基類的虛擬函式重新定義。所以虛擬函式只能用於類的繼承層次結構中。      一個成員函式被宣告為虛擬函式後,在同一類族中的類就不能

關於c++虛擬函式和介面的關係區分(簡單)

虛擬函式:                 虛擬函式的作用是實現動態聯編,也就是在程式的執行階段動態地選擇合適的成員函式,在定義了虛擬函式後,可以在基類的派生類中對虛擬函式重新定義,在派生類中重新定義的函式應與虛擬函式具有相同的形參個數和形參型別。以實現統一的介面,不同定義

C++虛擬函式表儲存位置淺析

關於C++中虛擬函式表,我們知道這樣一些事實: 1. 當class中存在virtual函式時,編譯器會為這個class追加一個void** __vfptr資料成員。 2. C++程式執行時,實際函式的呼叫,是通過查詢__vfptr來獲取的,從而實現多型。 3. 多型的實現,

淺析C++虛擬函式的呼叫及物件的內部佈局

     在我那篇《淺析C++中的this指標》中,我通過分析C++程式碼編譯後生成的彙編程式碼來分析this指標的實現方法。這次我依然用分析C++程式碼編譯後生成的彙編程式碼來說明C++中虛擬函式呼叫的實現方法,順便也說明一下C++中的物件內部佈局。下面所有的彙編程式碼都是

[C/C++]C++虛擬函式的原理和虛擬函式

#include using namespace std; class A{     public:     A();     virtual void fun1();     void fun2(); }; A::A() { } void A::fun1() {     cout<<"I am

C++虛擬函式不能是inline函式的原因

在C++中,inline關鍵字和virtual關鍵字分別用來定義c++中的行內函數和虛擬函式,他們在各自的場合都有其各自的應用,下面將簡單介紹他們各自的功能,然後在說明為什麼一個函式不能同時是虛擬函式和行內函數(inline)

C++類物件虛擬函式與多型性的實現

在面向物件程式設計時,有時會遇到這種需求:我們希望同一個方法在基類和派生類中實現不同的功能,即體現出行為上的多型性。一般有兩種方法可以實現這種需求,其一是在派生類中重新定義基類中方法,其二是使用虛擬函式。這裡主要記錄利用虛擬函式實現多型性的方法。 類中虛擬函式的定義方法 虛擬函式

C++純虛擬函式和Java的抽象函式區別

一直我對c++純虛擬函式的理解有個誤區:純虛擬函式是不能給出定義的。果然是學藝不精。c++中的純虛擬函式和java中的抽象函式很類似,我把相關概念搞混淆了,這裡總結一下:java中的抽象函式是隻有函式

C++建構函式作用

建構函式用於解決類中的物件初始化的問題 建構函式是一類特殊的函式,與其他的成員函式不同的是建構函式建構函式不需要使用者來呼叫它,而是建立物件的時候自動的執行 #include <iostr

C++】在繼承虛擬函式、純虛擬函式、普通函式,三者的區別

1.虛擬函式(impure virtual)   C++的虛擬函式主要作用是“執行時多型”,父類中提供虛擬函式的實現,為子類提供預設的函式實現。   子類可以重寫父類的虛擬函式實現子類的特殊化。   如下就是一個父類中的虛擬函式: class A { public: virt

C++建構函式、解構函式虛擬函式及普通成員函式的理解

這裡我們主要討論建構函式、解構函式、普通成員函式、虛擬函式,對這幾種函式說說自己的理解。 對建構函式的總結 對建構函式,我們先來看看如下的程式碼 #include <iostream> using namespace std; cla

C++建構函式能呼叫虛擬函式嗎?

      環境:XPSP3 VS2005         今天黑總給應聘者出了一個在C++的建構函式中呼叫虛擬函式的問題,具體的題目要比標題複雜,大體情況可以看如下的程式碼: class Base { public: Base() { Fuction(); }

C++ 在繼承虛擬函式、純虛擬函式、普通函式,三者的區別【轉載】

1.虛擬函式(impure virtual)   C++的虛擬函式主要作用是“執行時多型”,父類中提供虛擬函式的實現,為子類提供預設的函式實現。   子類可以重寫父類的虛擬函式實現子類的特殊化。   如下就是一個父類中的虛擬函式: class A { public

C++ 在繼承虛擬函式、純虛擬函式、普通函式,三者的區別

1.虛擬函式(impure virtual)   C++的虛擬函式主要作用是“執行時多型”,父類中提供虛擬函式的實現,為子類提供預設的函式實現。   子類可以重寫父類的虛擬函式實現子類的特殊化。   如下就是一個父類中的虛擬函式: class A { publi

C++ 函式的前面加上static的作用

在一般的函式前面加上static,作用是: 加了static後表示該函式失去了全域性可見性,只在該函式所在的檔案作用域內可見 當函式宣告為static以後,編譯器在該目標編譯單元內只含有該函式的入口地址,沒有函式名,其它編譯單元便不能通過該函式名來呼叫該函