虛析構函數? vptr? 指針偏移?多態數組? delete 基類指針 內存泄漏?崩潰?

http://blog.csdn.net/jnu_simba/article/details/12621955


五條基本規則:


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() 內有釋放內存資源的操作,那麼一定是存在內存泄漏的。


發佈了5 篇原創文章 · 獲贊 1 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章