1. 程式人生 > >【C++】C++淺拷貝、深拷貝及引用計數淺析

【C++】C++淺拷貝、深拷貝及引用計數淺析

在C++開發中,經常遇到的一個問題就是與指標相關的記憶體管理問題,稍有不慎,就會造成記憶體洩露、記憶體破壞等嚴重的問題。不像Java一樣,沒有指標這個概念,所以也就不必擔心與指標相關的一系列問題,但C++不同,從C語言沿襲下來的指標是其一大特點,我們常常要使用new/delete來動態管理記憶體,那麼問題來了,特別是伴隨著C++的繼承機制,如野指標、無效指標使用、記憶體洩露、double free、堆碎片等等,這些問題就像地雷一樣,一不小心就會踩那麼幾顆。

先來談一下C++類中常見的淺拷貝問題,以及由此引發的double free。什麼是淺拷貝?當類中的成員變數包括指標時,而又沒有定義自己的拷貝建構函式,那麼在拷貝一個物件的情況下,就會呼叫其預設拷貝建構函式,其實這個函式沒做什麼事,只是對其成員變數作了個簡單的拷貝,也就是所謂的位拷貝,它們指向的還是同一個儲存空間,當物件析構時,就會析構多次,也就是double free,下面舉例說明。

class Common
{
public:
    Common()
    {
        std::cout << "Common::Common" << std::endl;
    }

    Common(const Common &r)
    {
        std::cout << "Common::Common copy-constructor" << std::endl;
    }

    ~Common()
    {
        std::cout << "Common::~Common"
<< std::endl
; }
};

類Common是個一般的類,定義了構造、拷貝構造和解構函式,在函式裡輸出一些log,用以跟蹤函式呼叫情況。

class BitCopy
{
public:
    BitCopy()
        : m_p(new Common)
    {
        std::cout << "BitCopy::BitCopy" << std::endl;
    }

    ~BitCopy()
    {
        std::cout << "BitCopy::~BitCopy" << std
::endl; if (m_p) { delete m_p; m_p = NULL; } } private: Common *m_p; };

類BitCopy就是一個淺拷貝類,成員變數是我們剛定義的類指標,建構函式例項化成員變數,解構函式delete成員變數,沒有定義拷貝建構函式。

int main()
{
    BitCopy a;
    BitCopy b(a);
    return 0;
}
log如下:
Common::Common
BitCopy::BitCopy
BitCopy::~BitCopy
Common::~Common
BitCopy::~BitCopy
Common::~Common
*** Error in `./a.out': double free or corruption (fasttop): 0x0000000001f4e010 ***
已放棄 (核心已轉儲)

從上面的log可以看出,物件a呼叫了建構函式,物件b呼叫的是預設拷貝建構函式,最後析構了兩次,從而造成double free,核心已轉儲即core dump。

針對以上問題,該怎麼解決呢?有兩個辦法,一個是深拷貝,一個是引用計數。先來看一下深拷貝,深拷貝要定義自己的拷貝建構函式,在函式中給成員變數重新分配儲存空間,也就是所謂的值拷貝,這樣它們所指向的就是不同的儲存空間,析構時不會有問題,但這種方法只適用於較小的資料結構,如果資料結構過大,多次分配儲存空間之後,剩餘的儲存空間將逐漸減小,下面看個例子。

class ValueCopy
{
public:
    ValueCopy()
        : m_p(new Common)
    {
        std::cout << "ValueCopy::ValueCopy" << std::endl;
    }

    ValueCopy(const ValueCopy &r)
        : m_p(new Common(*r.m_p))
    {
        std::cout << "ValueCopy::ValueCopy copy-constructor" << std::endl;
    }

    ~ValueCopy()
    {
        std::cout << "ValueCopy::~ValueCopy" << std::endl;
        if (m_p) {
            delete m_p;
            m_p = NULL;
        }
    }

private:
    Common *m_p;
};

類ValueCopy是個深拷貝類,與上面例子的淺拷貝類不同的是定義了拷貝建構函式,在函式中給成員變數重新分配儲存空間,下面是用法及log。

int main()
{
    ValueCopy c;
    ValueCopy d(c);
    return 0;
}
Common::Common
ValueCopy::ValueCopy
Common::Common copy-constructor
ValueCopy::ValueCopy copy-constructor
ValueCopy::~ValueCopy
Common::~Common
ValueCopy::~ValueCopy
Common::~Common

從上面的log可以看出,物件c呼叫了建構函式,物件d呼叫的是自定義拷貝建構函式,最後析構了兩次而沒有問題,可見深拷貝的用處所在。

引用計數與深拷貝不同,方法是共享同一塊儲存空間,這個對大的資料結構比較有利。使用引用計數,需要在類中定義一個成員變數專門用於計數,初始值為1,後面引用了這個物件就加1,物件銷燬時引用減1,但並不真正的delete這個物件,只有當這個成員變數的值為0時才進行delete,例子如下。

class A
{
public:
    A()
        : m_refCount(1)
    {
        std::cout << "A::A" << std::endl;
    }

    A(const A &r)
        : m_refCount(1)
    {
        std::cout << "A::A copy-constructor" << std::endl;
    }

    ~A()
    {
        std::cout << "A::~A" << std::endl;
    }

    void attach()
    {
        std::cout << "A::attach" << std::endl;
        ++m_refCount;
    }

    void detach()
    {
        if (m_refCount != 0) {
            std::cout << "A::detach " << m_refCount << std::endl;
            if (--m_refCount == 0) {
                delete this;
            }
        }
    }

private:
    int m_refCount;
};

class B
{
public:
    B()
        : m_pA(new A)
    {
        std::cout << "B::B" << std::endl;
    }

    B(const B &r)
        : m_pA(r.m_pA)
    {
        std::cout << "B::B copy-constructor" << std::endl;
        m_pA->attach();
    }

    ~B()
    {
        std::cout << "B::~B" << std::endl;
        m_pA->detach();
    }

private:
    A* m_pA;
};

類A用到了引用計數,構造和拷貝建構函式都初始化為1,attach()函式為引用加1,detach()函式為引用減1,當引用計數值為0時delete物件。類B中的成員變數有個指標指向A,拷貝建構函式中呼叫了attach(),解構函式中呼叫了detach(),這樣也是一種保護,不會有記憶體洩露,也不會有double free,log如下。

int main()
{
    B e;
    B f(e);
    return 0;
}
A::A
B::B
B::B copy-constructor
A::attach
B::~B
A::detach 2
B::~B
A::detach 1
A::~A

從log中可以看出,指標成員變數的引用計數為2,這是正確的,最後正確delete,沒有問題。

在類中只要有指標成員變數,就要注意以上問題,另外,operator=這個賦值操作符也要在適當的時候進行過載。有時候,如果想規避以上問題,可以宣告拷貝建構函式和operator=操作符為private而不去實現它們。