深入理解虛函數

前言

在C++中,在基類中被聲明爲virtual並在在一個或多個派生類中被重新定義的成員函數就是虛函數。基本格式如下:

virtual (return_type) (func_name) (arg) {function body;}

我們可以通過指向派生類的基類指針或引用來調用派生類中同名覆蓋的成員函數。

如下代碼:

class A
{
    public:
        virtual void print(){cout<<"This is A"<<endl;}
};
class B : public A
{
    public:
    void print(){cout<<"This is B"<<endl;}
};

int main()
{
    A a;
    B b;
    A *p1=&a;
    A *p2=&b;
    p1->print();//This is A
    p2->print();//This is B
    return 0;
}

究竟虛函數底層是如何實現多態調用的呢,那麼下面就讓我們來深度剖析下虛函數這個機制。


虛函數的底層實現

首先看如下兩個類:

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;}
};

雖然類A跟B沒有任何的數據成員,但是由於虛函數的存在,編譯器在編譯的時候會在兩個類實例對象分別插入一個指向虛函數表vtbl的指針vptr。每個類都有自己的vtbl,當中保存着自己類中虛函數的地址。如下面圖所示:

這裏寫圖片描述

現在如果有如下代碼

A *p=new A;
p->fun();//調用A::fun

上面代碼是如何調用的呢?其實首先程序是先取出vptr的值,也就是vtbl的地址,根據這個值定位到vtbl,由於調用的函數A::func()是第一個虛函數,所以程序就取出vtbl的第一個slot的值即爲第一個虛函數的地址,也就是A::fun()的地址,最後調用這個函數。
由上述分析可知,只要vptr不同,指向的vtbl就不同,不同的vtbl裏裝着對應的虛函數地址。


實例分析

下面我們通過分析一個例子來驗證虛函數的底層實現,代碼如下:

#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(*fun)(A*);
    A *p=new B;
    long long lVptrAddr;
    memcpy(&lVptrAddr,p,8);
    memcpy(&fun,reinterpret_cast<long long*>(lVptrAddr),8);
    fun(p);
    delete p;
    return 0;
}

分析代碼:
1. void(fun)(A):首先我們定義了一個函數指針fun,其返回值爲void,參數爲A*。這個函數指針用來保存從vtbl取出的函數地址。
2. A *p=new B:接着我們new 一個B類的實例,將其申請的內存單元的地址的指針保存在A類型的指針p中,其實保存的就是vptr指針。
3. 接着我們定義一個long long類型來保存vptr的值,因爲我用的編譯器是64位的,指針的大小是64位,所以需要用long long類型來保存。
4. memcpy(&lVptrAddr,p,8): 該函數把p所指的8字節內存裏的值複製到lVptrAddr裏,複製的就是vptr指針指向的值,也就是vtbl的地址。
5. memcpy(&fun,reinterpret_cast<long long*>(lVptrAddr),8):取出vtbl中第一個slot的內容,並存放在函數指針fun裏。由於lVptrAddr是vtbl的地址,但lVptrAddr不是指針,我們可以使用reinterpret_cast將其轉換成long long *指針類型。
6. func(p):調用剛纔取出的函數地址裏面的函數,也就是B::fun()函數。
7. 如果想取出B::fun2()的話只需修改第五步的代碼爲memcpy(&fun,reinterpret_cast<long long*>(lVptrAddr)+1,8),依次類推。


虛函數需要注意的點:

(1)非類的成員函數不能定義爲虛函數,類的成員函數中靜態成員函數和構造函數也不能定義爲虛函數,但可以將析構函數定義爲虛函數。實際上,優秀的程序員常常把基類的析構函數定義爲虛函數。因爲,將基類的析構函數定義爲虛函數後,當利用delete刪除一個指向派生類定義的對象指針時,系統會調用相應的類的析構函數。而不將析構函數定義爲虛函數時,只調用基類的析構函數。

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

(3)當將基類中的某一成員函數聲明爲虛函數後,派生類中的同名函數(函數名相同、參數列表完全一致、返回值類型相關)自動成爲虛函數。

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


參考:

https://baike.baidu.com/item/%E8%99%9A%E5%87%BD%E6%95%B0/2912832?fr=aladdin

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章