使用C++為對象分配與釋放內存時的幾個好習慣


本文為大便一籮筐的原創內容,轉載請注明出處,謝謝:http://www.cnblogs.com/dbylk/


最近在為公司的項目寫內存泄漏定位工具,遇到一些關於C++構造與析構對象的問題,在此記錄一下。




一、不要混用 new/delete 和 new[]/delete[]

在默認情況下,也就是不存在 operator new 的重載時,new一個自定義類型 ClassA 的對象時,C++ 會先調用 malloc 來申請一塊 sizeof(ClassA) 大小的內存(操作系統會記錄這塊內存的首地址與大小),然后調用 ClassA 的構造函數在這塊內存上初始化對象。此時,new 關鍵字會返回 malloc 得到的地址。調用delete時,會首先執行 ClassA 的析構函數,再調用 free 釋放 malloc 得到的指針。


而new[]則稍微復雜一點,當你調用 new ClassA[nCount] 申請一個對象個數為 nCount 的 ClassA 數組時,編譯器(MSVC)會調用 malloc 申請一塊大小為 sizeof(ClassA) * nCount + 4 的內存,多出來的 4 bytes 被放在 new[] 關鍵字返回地址 ptr 的前面,其中記錄了數組中元素的個數。當調用 delete[] 刪除數組時,會根據數組首地址前 4 bytes 中記錄元素的個數來依次調用數組中對象的析構函數(每次指針偏移 sizeof(ClassA) 大小),再調用 free 釋放指針 (ptr - 4)。


因此,混用 new/delete 和 new[]/delete[] 通常會導致內存訪問崩潰。然后這里用了“通常”,也就是說在某些特定情況下,混用 new/delete 和 new[]/delete[] 是不會有任何影響的:

  1. 創建和釋放 C++ 的內建(build-in)類型時,即 int、char等。
  2. 創建和釋放“自身和所有成員變量都不含自定義構造函數和析構函數”的類型。(這一條可能依賴於編譯器的實現,至少在 MSVC 中此情況成立)

然而當項目代碼一旦復雜起來,要分清什么時候上面兩個條件能夠成立就不是那么輕松的事了,因此最好的方法就是無論何時何地都不要混用 new/delete 和 new[]/delete[]。


二、不要 delete “void” 指針

在整理公司項目代碼的過程中,發現有很多地方出現了類似於下面形式的代碼:

// Author :大便一籮筐 2016-04-03

struct StructA
{
    char cData;
}

void* pBuffer = new StructA[nSize];
// do something...
delete pBuffer;

可能寫過類似代碼的同學會覺得這種寫法並沒有什么問題,事實也是如此,它能夠正常工作,既不會產生內存泄漏,也不會運行報錯。

但是,上面情況只能說是一種幸運的巧合,如果發生一些微小的改變,結果就會發生意想不到的變化:

// Author :大便一籮筐 2016-04-03

struct StructA
{
    string strData;
}

void* pBuffer = new StructA[nSize];
// do something...
delete pBuffer;

細心的同學可能已經看出來了,由於 pBuffer 是 void 指針,delete pBuffer 時,並不會調用 StructA 的析構函數,而這導致了 string 的析構函數也沒有被調用,最終產生的結果就是 string 中的字符串緩沖泄漏。
有的同學可能會說,C++ 不是支持多態嘛,我把 StructA 的析構函數定義成虛函數不就好了。然而不幸的是,作為 C++ 的內建類型,void 並沒有定義析構函數,因此寄希望於多態是行不通的。
還有的同學可能會說,如果想定義 void 類型的內存緩沖區怎么辦?    —— 別忘記我們還有 malloc 和 free。
所以,任何時候都不要嘗試 delete void 指針。


三、盡量不要手動調用析構函數

看下面的代碼,你能看出程序輸出結果是什么嗎?

// Author :大便一籮筐 2016-03-31

class Base
{
public:
    Base() {};
    virtual ~Base()
    {
        cout << "Base has been destructed. " << endl;
    }

    void Release()
    {
        this->~Base();
    }

    int baseData;
};

class Derive : public Base
{
public:
    Derive() {};
    virtual ~Derive()
    {
        cout << "Derive has been destructed. " << endl;
    }

    void Releaase()
    {
        this->~Derive();
    }

    int deriveData;
};

int main()
{
    Base* pObject = new Derive();

    pObject->Release();
    delete pObject;

    system("pause");

    return 0;
}

我想很多同學會覺得答案是這個:

Derive has been destructed.
Base has been destructed.
Derive has been destructed.
Base has been destructed.

然而很不幸的,正確答案是這個:

Derive has been destructed.
Base has been destructed.
Base has been destructed.

因為在手動調用 pObject 的析構函數時,雖然 pObject 所指向的內存空間並沒有被釋放,但執行完 Derive 的析構函數后, pObject 所指向對象的虛函數表指針會從指向派生類 Derive 的虛函數表恢復為指向基類 Base 的虛函數表。
即手動調用析構函數之后,多態指針 pObject 失去了自己的多態特性,此時無法再通過 pObject 直接調用派生類中的虛函數。因此,最后 delete 時只會調用基類的析構函數。

所以,盡量不要在自己寫的函數中手動調用析構函數是一個好習慣。

然而上面提到了“盡量”,那就是說事情並沒有那么絕對,C++ 支持手動調用析構函數,自然有它的道理:當你需要自己寫內存管理器時,手動調用析構函數是必須的。


免責聲明!

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



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