Shallow Copy 和 Deep Copy

在前一章,我們終於學到了什麼是 copy assignment operator。

今天我們要來講一個超級重要的概念:Shallow Copy 和 Deep Copy!

這個概念涉及到了記憶體的處理,如果處理不當,很容易造成記憶體洩漏,也就是所謂的 memory leak。

Shallow Copy

什麼是 Shallow Copy

當然,一開始我們要先來講講什麼是 shallow copy。

首先,我們來回顧一下, copy constructor 複製建構子 以及 copy assignment operator 複制指定運算子 的基本運作邏輯,都是將一個值,複製給一個物件。

如果一個類別有三個特徵成員,那 copy constructor 或是 copy assignment operator 在執行時,就會依序複製三個成員的值。

這樣的複製機制,我們不需要特別去寫,C++ 都已經幫我們在背後做好了。

而這樣的預設複製機制,我們叫做 shallow copy。

複習 copy constructor 和 assignment operator

我們再來複習一下 copy constructor 和 copy assignment operator 他們的樣子:

class Dollars
{
private:
    int m_dollars { 0 };

public:
    // constructor
    Dollars(int dollars)
        : m_dollars{ dollars }
    {}

    // 預設 copy constructor
    Dollars(const Dollars& d)
        : m_dollars{ d.m_dollars }
    {}

    // 預設 assignment operator
    Dollars& operator= (const Dollars& dollars)
    {
        if (this == &dollars)
            return *this;

        m_dollars = dollars.m_dollars;
        return *this;
    }
};

上面的程式碼中,我們實作了「預設」的 copy constructor 和 copy assignment operator。

之所以叫「預設」,是因為實際上我們不需要寫出來,C++ 也會幫我們自動生成類似這樣的程式碼。

而在這「預設」函數所實施的複製行為,就叫做 shallow copy!

有什麼問題

這個機制看起來很正常啊,會有什麼問題嗎?

其實這樣的機制,大部分情況下都沒有問題。但如果涉及到動態的記憶體配置,那就會有問題了!

什麼是動態記憶體配置?

簡單來說,就是記憶體的配置是在程式執行時才進行的。因為是在程式執行時在進行,因此我們可以使用變數決定陣列大小,或是釋放記憶體。

最常見的一個例子就是 指標 pointer 的應用。

我們來看一個簡單的例子:

class ShallowCopy
{
public:
    int* data;

    // Constructor
    ShallowCopy(int value) {
        data = new int(value);
    }

    // Destructor
    ~ShallowCopy() {
        delete data;
    }

    // Copy Constructor (Shallow Copy)
    ShallowCopy(const ShallowCopy& other) {
        data = other.data;
    }
};

在這個類別中,我們有一個指向整數型態的一個指標。

然後我們實作了一個簡單的 copy constructor,就像之前一樣。

現在我們來看看,執行以下的程式碼會發生什麼事:

int main()
{
    ShallowCopy obj1(42);
    ShallowCopy obj2 = obj1;  // Shallow copy

    std::cout << "obj1 data: " << *obj1.data << std::endl;
    std::cout << "obj2 data: " << *obj2.data << std::endl;

    // 更改 obj1 的 data
    *obj1.data = 24;
    std::cout << "更改後:" << std::endl;
    std::cout << "obj1 data: " << *obj1.data << std::endl;
    std::cout << "obj2 data: " << *obj2.data << std::endl;

    return 0;
}

我們會看到類似這樣的輸出:

obj1 data: 42
obj2 data: 42
更改後:
obj1 data: 24
obj2 data: 24
free(): double free detected in tcache 2
Aborted

誒有沒有發現,更改 obj1 的值居然也會更改到 obj2 的值!

除了這個奇怪的點,C++ 好像還回報了某種錯誤?

這是為什麼???

這是因為當我們在做 shallow copy 的時候,我們實際上只複製了指標,並沒有複製被指到的物件!所以其實,當 copy constructor 被執行後,會有兩個指標同時指向同一個物件!

如下圖:

這樣看就很清楚的知道為什麼更改 obj1 的資料會連帶更改到 obj2 的資料了吧!

那 C++ 回報的錯誤又是什麼呢?

這就和解構子有關了,因為當 obj1 的解構子被執行時,C++ 就會去刪掉那個資料。那麼當 obj2 的解構子被執行時,C++ 又會嘗試去刪除那個已經被刪除的資料,因為 C++ 找不到那個資料,因此會回報錯誤。

那知道原因了,我們要如何解決這個問題呢?

所以接下來我們講到 Deep Copy。

Deep Copy

其實要解決前面遇到的問題很簡單,就是把指到的資料,也就是上圖中的 data,也同樣複製一個。這就是 deep copy 的核心概念!

我們直接來看解法:

class DeepCopy
{
public:
    int* data;

    // Constructor
    DeepCopy(int value) {
        data = new int(value);
    }

    // Destructor
    ~DeepCopy() {
        delete data;
    }

    // Copy Constructor (Deep Copy)
    DeepCopy(const DeepCopy& other) {
        data = new int(*other.data);
    }
};

int main() {
    DeepCopy obj1(42);
    DeepCopy obj2 = obj1;  // Deep copy

    std::cout << "obj1 data: " << *obj1.data << std::endl;
    std::cout << "obj2 data: " << *obj2.data << std::endl;

    // 更改 obj1 的 data
    *obj1.data = 24;
    std::cout << "更改後:" << std::endl;
    std::cout << "obj1 data: " << *obj1.data << std::endl;
    std::cout << "obj2 data: " << *obj2.data << std::endl;

    return 0;
}

接著執行程式碼,我們可以看到現在更改其中一個物件的資料不會再連帶更改到另一個物件了!

這是因為我們用了關鍵的指令,來複製資料本身。

data = new int(*other.data);

圖像化理解就是像這樣:

這樣一來,前面遇到的問題就解決啦!

同時,執行 destructor 也不會讓 C++ 找不到資料。

是不是其實很簡單!

總結

這篇我們學到了一個超級重要的概念!

想當初面試的時候,常常就會被問到 shallow copy 和 deep copy 的差別在哪裡,以及該如何修正。

這個概念讓我們更近一步認識了 C++ 以及指標的概念。雖然概念很簡單,但是即使是有經驗的工程師也是很容易犯下這樣的錯誤!

那這篇就到這裡啦!有學到東西的話歡迎留五星評價喔!