1. 程式人生 > >探究C++中的成員函式指標和虛擬函式表

探究C++中的成員函式指標和虛擬函式表

say something

相信對C++物件有一定了解的話,應該都會知道,在C++中物件的實現中,成員函式和成員變數是分離的
所以我們所談到的非靜態成員函式其實只是一個普通的函式不過被編譯器所隱藏,必須繫結到特定的物件上才能執行
靜態成員函式實際上就真的就是一個普通的函式,獨立於整個物件之外,不過被編譯器加上了一堆修飾避免重新命名,和物件無關
普通非靜態成員函式的實現是通過傳入this指標的方式去繫結到特定的物件上,然後執行特化的操作

我們先定義一個測試物件,下面的所有操作都是基於這個類

class Base {
private:
    int a;

public:
    Base():a(0
) {} void test() { cout << "call Base ::test()" << endl; }//沒有操作成員變數 void testMember() { a = 6; }//操作了成員變數 static void testStatic() { cout << "call Base::testStatic"<<endl; }//靜態成員函式 virtual void test1() { cout << "call Base ::test1()" << endl; }//虛成員函式1 virtual
void test2()const { cout << "call Base ::test2()" << endl; }//const虛成員函式2 virtual void test3(int a) { cout << "call Base ::test3(int a)" << endl; }//帶引數的虛成員函式3 };

非靜態成員函式指標

我們可以通過如下的方式去宣告以及定義這個物件的非靜態成員函式指標

    void(Base::*func1)();//定義一個名為func1的指向Base物件的函式指標,未初始化
    //func1 = &Base::testStatic;//報錯,這裡提示testStatic是一個型別為void (void)的函式,驗證了我的觀點
func1 = &Base::test;//繫結到指定成員函式上去,現在其值為一個例項化函式的地址

定義完成,如何去使用這個函式指標?
我們可以有多種方式去呼叫,不過無論哪種方式方式都需要將函式指標繫結到例項化的物件上:

    Base src;
    (src.*func1)();//直接呼叫

    Base* ptr = &src;
    (ptr->*func1)();//指標呼叫

    //實際上,無論如何呼叫,編譯器都會將上面的呼叫轉換成下面的呼叫方式
    (*func1)(&src);
    (*func1)(ptr);
    //當然,你直接這麼寫是不行的,只有編譯器可以這麼做

靜態成員函式指標

前面已經說了,在編譯器的眼裡,靜態成員函式實際上和普通函式沒什麼不同,上面的成員函式可能被編譯器修飾成如下模樣並直接放在全域性:

extern void _Base_testStaticVoid_1() { cout << "call Base::testStatic" << endl; }

我們呼叫的時候就直接被編譯器解釋成這個模樣:

    Base::testStatic();
    //解釋成如下:
    _Base_testStaticVoid_1();

這也和靜態函式的作用有關,靜態成員函式並不需要和特定物件進行聯絡,而是隻能操作靜態成員和區域性以及全域性變數。
所以,我們如何定義一個靜態成員函式指標?——就像定義普通函式指標那樣

typedef void(*Func)();//簡單的typedef,將Func表示為一個void(void)型別的函式的指標型別
Func f1 = &Base::testStatic;//直接定義和賦值

就像操作普通函式指標那樣

虛擬函式表

要說到虛擬函式表的話,希望大家已經知道C++為每一個定義有虛擬函式的物件都會構建一個虛擬函式表,虛擬函式表中儲存了每一個虛擬函式的例項函式的地址
一般當我們定義一個物件:

Base *a = new Base();

這時候,編譯器做了什麼呢?不僅僅是做了為我們的物件分配記憶體空間然後按照建構函式去構建物件的操作,還做了一些額外的操作:

Base():a(0) {}
會被拓展成:
Base():a(0) {
  this->vptr=Base::vptr;
  ...
  //為每一個含有建構函式的成員變數進行一一的構建
  ...
  //自己寫的操作
}

它會多分配四個位元組或者八個位元組的空間去儲存一個指標變數,這個指標變數指向虛擬函式表的頭部,一般編譯器會將這個指標放在物件的頭部。然後在每次構建的時候去修改這個虛擬函式指標的值,確保多型的正確執行。
然後在每次呼叫虛擬函式的時候都會找到相應型別的物件的虛擬函式表指標對應的虛擬函式表中尋找對應的那一個虛擬函式

我們可以試著去探尋一下我說的是否正確:

void TestVirtualTable() {
    Base *a = new Base();
    int* vptr = reinterpret_cast<int*>(a);//獲得虛擬函式表的指標,指向虛擬函式表的第一個函式
    int p = *vptr;
    Func* funcT = reinterpret_cast<Func*>(p);//將函式表的第一個函式的例項轉換成函式指標
    funcT[0]();
    funcT[1]();
    Func1 f1 = reinterpret_cast<Func1>(funcT[2]);
    f1(1);     //可以輸出,但是會報錯
}

輸出如下:

call Base ::test1()
call Base ::test2()
call Base ::test3(int a)

reinterpret_cast我就不再贅述了,可以看到,的確如我所說,虛擬函式表指標就放在Base*指向的位置。
那麼,我們該如何定義一個虛擬函式指標呢?
其實和普通成員函式指標有點類似:

    void (Base::*func)();
    func = &Base::test1;

我們已經知道,對一個非靜態成員函式取其地址,將獲得這個函式在記憶體中的地址,但是對一個虛擬函式取地址的話,由於其地址在編譯時期是不能確定的(多型),所以我們只能獲得一個索引值

比如針對上面的程式碼,我們輸出&Base::test1的話,會發現其值為1(當然我們是不能直接進行輸出),&Base::test2的話,輸出為2,這個索引值代表了其在虛擬函式表中的位置

所以如果我們通過一個虛擬函式指標“去執行函式的話:

void (Base::*func)()= &Base::test1;
Base bs;
(bs.*func)();

(bs.*func)();這一行程式碼會被修飾成如下形式:

(bs.vptr[(int)func])(&bs);

只有這樣,才能很好的支援繼承的關係

所以,我們可以自己思考一下,多型是怎麼通過這種方式去實現的?