C++ 多態的實現原理與內存模型


  多態在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.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM