虛解構函式? vptr? 指標偏移?多型陣列? delete 基類指標 記憶體洩漏?崩潰?

s1mba發表於2013-10-11

五條基本規則:


1、如果基類已經插入了vptr, 則派生類將繼承和重用該vptr。vptr(一般在物件記憶體模型的頂部)必須隨著物件型別的變化而不斷地改變它的指向,以保證其值和當前物件的實際型別是一致的。


2、在遇到通過基類指標或引用呼叫虛擬函式的語句時,首先根據指標或引用的靜態型別來判斷所調函式是否屬於該class或者它的某個public 基類,如果

屬於再進行呼叫語句的改寫:

 C++ Code 
1
(*(p->_vptr[slotNum]))(p, arg-list);

其中p是基類指標,vptr是p指向的物件的隱含指標,而slotNum 就是呼叫的虛擬函式指標在vtable 的編號,這個陣列元素的索引號在編譯時就確定下來,

並且不會隨著派生層的增加而改變。如果不屬於,則直接呼叫指標或引用的靜態型別對應的函式,如果此函式不存在,則編譯出錯。


3、C++標準規定對物件取地址將始終為對應型別的首地址,這樣的話如果試圖取基類型別的地址,將取到的則是基類部分的首地址。我們常用的編譯器,如vc++、g++等都是用的尾部追加成員的方式實現的繼承(前置基類的實現方式),在最好的情況下可以做到指標不偏移;另一些編譯器(比如適用於某些嵌入式裝置的編譯器)是採用後置基類的實現方式,取基類指標一定是偏移的。


4、delete[]  的實現包含指標的算術運算,並且需要依次呼叫每個指標指向的元素的解構函式,然後釋放整個陣列元素的記憶體。


5、 在類繼承機制中,建構函式和解構函式具有一種特別機制叫 “層鏈式呼叫通知” 《 C++程式設計思想 》

C++標準規定:基類的解構函式必須宣告為virtual, 如果你不宣告,那麼"層鏈式呼叫通知"這樣的機制是沒法構建起來.從而就導致了基類的解構函式被呼叫了,而派生類的解構函式沒有呼叫這個問題發生.


如下面的例子:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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;
}

按照上面的規則2,pI->Draw(200); 會編譯出錯,因為在基類並沒有定義Draw(int) 的虛擬函式,於是查詢基類是否定義了Draw(int),還是沒有,就出錯了,從出錯提示也可以看出來:“IRectangle::Draw”: 函式不接受 1 個引數。

此外,上述小例子還隱含另一個知識點,我們把出錯的語句遮蔽掉,看輸出:

Rectangle::Draw()
~Rectangle()
~IRectangle()

即派生類和基類的解構函式都會被呼叫,這是因為我們將基類的解構函式宣告為虛擬函式的原因,在pI 指向派生類首地址的前提下,如果~IRectangle() 

是虛擬函式,那麼會找到實際的函式~Rectangle() 執行,而~Rectangle() 會進一步呼叫~IRectangle()(規則5)。如果沒有這樣做的話,只會輸出基類的

解構函式,這種輸出情況通過比對規則2也可以理解,pI 現在雖然指向派生類物件首地址,但執行pI->~IRectangle() 時 發現不是虛擬函式,故直接呼叫

假如在派生類解構函式內有釋放記憶體資源的操作,那麼將造成記憶體洩漏。更甚者,問題遠遠沒那麼簡單,我們知道delete pI ; 會先呼叫解構函式,再釋

放記憶體(operator delete),上面的例子因為派生類和基類現在的大小都是4個位元組即一個vptr,故不存在釋放記憶體崩潰的情況,即pI 現在就指向派生

類物件的首地址。如果pI 偏離了呢?問題就嚴重了,直接崩潰,看下面的例子分析。


現在來看下面這個問題:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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;
}


輸出為:


由於基類的fun不是虛擬函式,故p->fun() 呼叫的是Base::fun()(規則2),而且delete p 還會崩潰,為什麼呢?因為此時基類是空類1個位元組,派生類有虛擬函式故有vptr 4個位元組,基類“繼承”的1個位元組附在vptr下面,現在的p 實際上是指向了附屬1位元組,即operator delete(void*) 傳遞的指標值已經不是new 出來時候的指標值,故造成程式崩潰。 將基類解構函式改成虛擬函式,fun() 最好也改成虛擬函式,只要有一個虛擬函式,基類大小就為一個vptr ,此時基類和派生類大小都是4個位元組,p也指向派生類的首地址,問題解決,參考規則3。


最後來看一個所謂的“多型陣列” 問題

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
using namespace std;

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

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

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

    delete [] pb;

    return 0;
}

由於sizeB != sizeD,參照規則4,pb[1] 按照B的大小去跨越,指向的根本不是一個真正的B物件,當然也不是一個D物件,因為找到的D[1] 虛擬函式表位置是錯的,故呼叫解構函式出錯。程式在g++ 下是segment fault  的,但在vs 中卻可以正確執行,在C++的標準中,這樣的用法是undefined 的,只能說每個編譯器實現不同,但我們最好不要寫出這樣的程式碼,免得庸人自擾。

delete-expression:
::opt delete cast-expression
::opt delete [ ] cast-expression

In the first alternative (delete object), if the static type of the operand is different from its dynamic type, the static type shall be a base class of the 

operand’s dynamic type and the static type shall have a virtual destructor or the behavior is undefined

In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.

第二點也就是上面所提到的問題。關於第一點。也是論壇上經常討論的,也就是說delete 基類指標(在指標沒有偏離的情況下) 會不會造成記憶體洩漏的問題,上面說到如果此時基類解構函式為虛擬函式,那麼是不會記憶體洩漏的,如果不是則行為未定義。

如下所示:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include<iostream>
using namespace std;

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

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

int main(void)
{
    cout << "sizeB:" << sizeof(B) << " sizeD:" << sizeof(D) << endl;
    D *pd = new D;
    B *pb = pd;
    cout << (void *)pb << endl;
    cout << (void *)pd << endl;

    delete pb;

    return 0;
}

現在B與D大小不一致,delete pb; 此時pb 沒有偏移,在linux g++ 下通過valgrind (valgrind --leak-check=full ./test )檢測,並沒有記憶體洩漏,基類和派生類的解構函式也正常被呼叫。

如果將B 的解構函式virtual 關鍵字去掉,那麼B與D大小不一致,而且此時pb 已經偏移,delete pb; 先呼叫~B(),然後free 出錯,如

*** glibc detected *** ./test: free(): invalid pointer: 0x09d0000c *** ,參照前面講過的例子。

如果將B和D 的virtual 都去掉,B與D大小不一致,此時pb 沒有偏移,delete pb; 只呼叫~B(),但用varlgrind 檢測也沒有記憶體洩漏,實際上如上所說,這種情況是未定義的,但可以肯定的是沒有呼叫~D(),如果在~D() 內有釋放記憶體資源的操作,那麼一定是存在記憶體洩漏的。


參考:

《高質量程式設計指南C++/C語言》

http://coolshell.cn/articles/9543.html

http://blog.csdn.net/unituniverse2/article/details/12302139

http://bbs.csdn.net/topics/370098480


相關文章