1. 程式人生 > >程式設計師面試一百題-15-含有指標成員的類的拷貝

程式設計師面試一百題-15-含有指標成員的類的拷貝

1-題目 :
下面是一個數組類的宣告與實現;請分析這個類有什麼問題,並針對存在的問題提出幾種解決方案。

//有問題的程式碼
template <typename T>
class Arr
{
  public:
    Arr(unsigned arrSize) : data(0), size(arrSize)
    {
        if (size > 0)
        {
            data = new T[size];
        }
    }

    ~Arr()
    {
        if (data)
        {
            delete[] data;
        }
    }

    void setValue(unsigned index, const T &value)
    {
        if (index < size)
        {
            data[index] = value;
        }
    }

    T getValue(unsigned index) const
    {
        if (index < size)
        {
            return data[index];
        }
        else
        {
            return T();
        }
    }

  private:
    T* data;
    unsigned size;
};

2-思路 :
軟體存在的大部分問題通常都可以歸結指標的不正確處理。這個類只提供了一個建構函式,而沒有定義構造拷貝函式過載拷貝運算子函式
當按下面的方式宣告並例項化該類的一個例項 : Arr A(10); Arr B(A); ,或者按下面的方式把該類的一個例項賦值給另外一個例項 : Arr A(10); Arr B(10); B=A;,編譯器將呼叫其自動生成的構造拷貝函式或者拷貝運算子的過載函式,預設是對指標實行按位拷貝,僅僅只是拷貝指標的地址,而不會拷貝指標的內容。
因此在執行完前面的程式碼之後,A.data和B.data指向的是同一地址。當A或者B中任意一個結束其生命週期呼叫解構函式時,會刪除data。由於他們的data指向的是同一個地方,兩個例項的data都被刪除了。但另一個例項並不知道它的data已經被刪除了

,當企圖再次用它的data的時候,程式就會不可避免地崩潰。
2.1-方案1 : 由於問題出現的根源是呼叫了編譯器生成的預設構造拷貝函式和拷貝運算子的過載函式。一個最簡單的辦法就是禁止使用這兩個函式。於是可以把這兩個函式宣告為私有函式,如果類的使用者企圖呼叫這兩個函式,將不能通過編譯。實現的程式碼如下 :

private:
    Arr(const Arr& copy);
    const Arr& operator = (const Arr& copy);

2.2-方案2 : 最初的程式碼存在問題是因為不同例項的data指向的同一地址,刪除一個例項的data會把另外一個例項的data也同時刪除。因此我們還可以讓構造拷貝函式或者拷貝運算子的過載函式拷貝的不只是地址,而是資料。由於我們重新儲存了一份資料,這樣一個例項刪除的時候,對另外一個例項沒有影響。這種思路我們稱之為深度拷貝

。實現的程式碼如下 :

public:
    Arr(const Arr &copy) : data(0), size(copy.size)
    {
        if (size > 0)
        {
            data = new T[size];
            for (unsigned int i = 0; i < size; i++)
            {
                setValue(i, copy.getValue(i));
            }     
        }
    }

    const Arr &operator=(const Arr &copy)
    {
        if (this == &copy)
            return *this;
        if (data != NULL)
        {
            delete[] data;
            data = NULL;
        }
        size = copy.size;
        if (size > 0)
        {
            data = new T[size];
            for (unsigned int i = 0; i < size; i++)
            {
                setValue(i, copy.getValue(i));
            }
        }
    }

2.3-方案3 : 為了防止有多個指標指向的資料被多次刪除,我們還可以儲存究竟有多少個指標指向該資料,只有當沒有任何指標指向該資料的時候才可以被刪除,這種思路通常被稱之為引用計數技術。在建構函式中,引用計數初始化為1;每當把這個例項賦值給其他例項或者以引數傳給其他例項的構造拷貝函式的時候,引用計數加1,因為這意味著又多了一個例項指向它的data;每次需要呼叫解構函式或者需要把data賦值為其他資料的時候,引用計數要減1,因為這意味著指向它的data的指標少了一個。當引用計數減少到0的時候,data已經沒有任何例項指向它了,這個時候就可以安全地刪除。實現的程式碼如下 :

public:
    Arr(unsigned ArrSize) : data(0), size(ArrSize), count(new unsigned int)
    {
        *count = 1;
        if (size > 0)
        {
            data = new T[size];
        }
    }

    Arr(const Arr &copy) : size(copy.size), data(copy.data), count(copy.count) { (*count)++; }
    ~Arr() { Release(); }

    const Arr &operator=(const Arr &copy)
    {
        if (data == copy.data)
        {
            return *this;
        }  
        Release();
        data = copy.data;
        size = copy.size;
        count = copy.count;
        (*count)++;
    }

private:
    void Release()
    {
        (*count)--;
        if (*count == 0)
        {
            if (data)
            {
                delete[] data;
                data = NULL;
            }
            delete count;
            count = 0;
        }
    }
    unsigned int *count;