C++學習筆記——虛函數

基本概念

虛函數在某基類中聲明爲 virtual 並在一個或多個派生類中被從新定義的成員函數用法格式爲:ios

virtual 函數返回類型 函數名(參數表) {函數體};程序員

C++中用它來實現多態性,經過指向派生類基類指針或引用,訪問派生類中同名覆蓋成員函數數組

虛函數實現機制

虛函數是如何作到因對象的不一樣而調用其相應的函數的呢?函數

如今咱們就來剖析虛函數。咱們先定義兩個類this

class A{//虛函數示例代碼
public:
virtual voidfun(){cout<<1<<endl;}
virtual voidfun2(){cout<<2<<endl;}
};

class B : public A{
public:
void fun(){cout<<3<<endl;}
void fun2(){cout<<4<<endl;}
};

因爲這兩個類中有虛函數存在,因此編譯器就會爲他們兩個分別插入一段你不知道的數據,併爲他們分別建立一個表。那段數據叫作vptr指針,指向那個表。那個表叫作vtbl。spa

每一個類都有本身的vtbl,vtbl的做用就是保存本身類中虛函數的地址,咱們能夠把vtbl形象地當作一個數組,這個數組的每一個元素存放的就是虛函數的地址,請看圖指針

經過左圖,能夠看到這兩個vtbl分別爲class A和class B服務。如今有了這個模型以後,咱們來分析下面的代碼code

A *p=new A;htm

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");
return 0;
}

用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)非類的成員函數不能定義爲虛函數,類的成員函數中靜態成員函數和構造函數也不能定義爲虛函數,但能夠將析構函數定義爲虛函數。實際上,優秀的程序員經常把基類析構函數定義爲虛函數。由於,將基類析構函數定義爲虛函數後,當利用delete刪除一個指向派生類定義的對象指針時,系統會調用相應的類的析構函數。而不將析構函數定義爲虛函數時,只調用基類的析構函數。

(2)只須要在聲明函數的類體中使用關鍵字「virtual」將函數聲明爲虛函數,而定義函數時不須要使用關鍵字「virtual」。

(3)當將基類中的某一成員函數聲明爲虛函數後,派生類中的同名函數自動成爲虛函數。

(4)若是聲明瞭某個成員函數爲虛函數,則在該類中不能出現和這個成員函數同名而且返回值、參數個數、類型都相同的非虛函數。在以該類爲基類派生類中,也不能出現這種同名函數。

虛函數和繼承

虛函數聯繫到多態,多態聯繫到繼承。

繼承有如下三種方式:

public
基類的public和protected的成員被派生類繼承後,保持原來的狀態
private 基類的public和protected的成員被派生類繼承後,變成派生類的private成員
protected 基類的public和protected的成員被派生類繼承後,變成派生類的protected成員

注:不管何種繼承方式,基類的private成員都不能被派生類訪問。

從上面的表中能夠看出:

聲明爲public的方法和屬性能夠被隨意訪問;

聲明爲protected的方法和屬性只能被類自己和其子類訪問;

而聲明爲private的方法和屬性只能被當前類的對象訪問。

相關文章
相關標籤/搜索