C++ 多型的實現原理與記憶體模型

KingsLanding發表於2014-08-09

  多型在C++中是一個重要的概念,通過虛擬函式機制實現了在程式執行時根據呼叫物件來判斷具體呼叫哪一個函式。

     具體來說就是:父類類別的指標(或者引用)指向其子類的例項,然後通過父類的指標(或者引用)呼叫實際子類的成員函式。在每個包含有虛擬函式的類的物件的最前面(是指這個物件物件記憶體佈局的最前面)都有一個稱之為虛擬函式指標(vptr)的東西指向虛擬函式表(vtbl),這個虛擬函式表(這裡僅討論最簡單的單一繼承的情況,若果是多重繼承,可能存在多個虛擬函式表)裡面存放了這個類裡面所有虛擬函式的指標,當我們要呼叫裡面的函式時通過查詢這個虛擬函式表來找到對應的虛擬函式,這就是虛擬函式的實現原理。注意一點,如果基類已經插入了vptr, 則派生類將繼承和重用該vptr。vptr(一般在物件記憶體模型的頂部)必須隨著物件型別的變化而不斷地改變它的指向,以保證其值和當前物件的實際型別是一致的。

  以上這些概念都是C++程式設計師很熟悉的,下面通過一些具體的例子來強化一下對這些概念的理解。

1. 

#include<iostream>
using namespace std;

class IRectangle
{
public:
    virtual ~IRectangle() {}
    virtual void Draw() = 0;
};

class Rectangle: public IRectangle
{
public:
    virtual ~Rectangle() {}
    virtual void Draw(int scale)
    {
        cout << "Rectangle::Draw(int)" << endl;
    }
    virtual void Draw()
    {
        cout << "Rectangle::Draw()" << endl;
    }
};

int main(void)
{
    IRectangle *pI = new Rectangle;
    pI->Draw();
    pI->Draw(200);
    delete pI;
    return 0;
}

  該段程式碼編譯失敗:

C:\Users\zhuyp\Desktop>g++ -Wall test.cpp -o test -g

test.cpp: In function 'int main()':
test.cpp:29:17: error: no matching function for call to 'IRectangle::Draw(int)'
pI->Draw(200);
^
test.cpp:29:17: note: candidate is:
test.cpp:8:18: note: virtual void IRectangle::Draw()
virtual void Draw() = 0;
^
test.cpp:8:18: note: candidate expects 0 arguments, 1 provided

C:\Users\zhuyp\Desktop>

  以上資訊表明,在父類IRectangle中並沒有Draw(int)這個函式。確實,在父類IRectangle中沒有這樣簽名的函式,但是不是多型嗎,new 的不是子類Rectangle嗎?我們注意到指標 pI 雖然指向子類,但是本身確是父類 IRectangle 型別,因此在執行 pI->draw(200)的時候查詢父類vtable,父類的vtable 中沒有Draw(int)型別的函式,因此編譯錯誤。

  如果將 pI->draw(200) 這一句修改,將pI進行一個down cast 則編譯正常,dynamic_cast<Rectangle *>(pI)->draw(200); 此時呼叫的是子類的指標,查詢的是子類的vtable,該vtable中有簽名為 draw(int) 的函式,因此不會有問題。

2.

#include <iostream>
using namespace std;

class Base
{
public:
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void fun()
    {
        cout << "Base::fun()"  << endl;
    }
};

class Derived : public Base
{
public:
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
    virtual void fun()
    {
        cout << "Derived::fun()"  << endl;
    }
};

int main()
{
    Derived *dp = new Derived;
    Base *p = dp;
    p->fun();
    cout << sizeof(Base) << endl;
    cout << sizeof(Derived) << endl;
    cout << (void *)dp << endl;
    cout << (void *)p << endl;
    delete p;
    p = NULL;

    return 0;
}

  編譯並執行程式:

C:\Users\zhuyp\Desktop>test.exe
Base::fun()
1
8
0x3856a0
0x3856a0
~Base()

  編譯器使用的是gcc4.8.1 可以看出 p 和 pb 的值是相同的,因此可以得出結論,現代C++編譯器已經沒有為了效能的問題將vptr指標放在類記憶體模型的最前面了。

3.

#include<iostream>
using namespace std;

class B
{
    int b;
public:
    virtual ~B()
    {
        cout << "B::~B()" << endl;
    }
};

class D: public B
{
    int i;
    int j;
public:
    virtual ~D()
    {
        cout << "D::~D()" << endl;
    }
};

int main(void)
{
    cout << "sizeB:" << sizeof(B) << " sizeD:" << sizeof(D) << endl;
    char *ch = NULL;
    
    B *pb = new D[2];

    cout<<"size *pb "<<sizeof(pb)<<"\tend"<<endl;
    
    delete [] pb;

    return 0;
}

  程式執行出錯,在輸出 pb 的大小之後。可見是在delete [] pb 的時候出了問題。

  我們知道釋放申請的陣列空間的時候需要使用 delete [] ,那 delete 怎麼知道要釋放多大的記憶體呢?delete[]  的實現包含指標的算術運算,並且需要依次呼叫每個指標指向的元素的解構函式,然後釋放整個陣列元素的記憶體。

  由於C++中多型的存在,父類指標可能指向的是子類的記憶體空間。由於上面的例子中delete [] 釋放的是多型陣列的空間,delete[] 計算空間按照 B 類的大小來計算,每次偏移呼叫解構函式是按照B類來進行的,而該陣列實際上存放的是D類的指標釋放的大小不對(由於 sizeof(B) != sizeof(D) ,),因此會崩潰。

C:\Users\zhuyp\Desktop>test.exe
sizeB:16 sizeD:24
size *pb 8 end

注意:本程式碼在64bit環境中執行的,因此 *pb 是 8.

 

相關文章