對於剛接觸 C++ 的新手來說,記憶體管理往往是最容易踩雷的地方。它不像 Java 或 Python 那樣,會在背後自動協助管理記憶體;在 C++ 中,我們必須時時留意記憶體的配置與釋放,才能避免程式變得不穩定,甚至造成記憶體洩漏。

Table of Contents
Picture 類別
本文章會重複以 Picture 類別物件,示範可能會導致懸空指標、重複刪除等等執行期錯誤的程式碼。 在 Picture 類別物件中,像素資料儲存在 heap 空間。當 Picture 物件生命週期結束後,要負責釋放它在 heap 空間的像素資料。
以下為 Picture 類別的定義 :
class Picture {
public:
// 影像寬、高
int w, h;
// 動態配置的 int 陣列,共有 w * h 個像素
int* pixels;
// 給定高和寬,建立 Picture 物件
Picture(int w, int h) : w(w), h(h) { pixels = new int[w * h]; }
// 釋放 this->pixels 的空間
~Picture() { delete[] pixels; }
};
記憶體洩漏 Memory Leak
程式向系統申請一塊記憶體,之後卻沒有釋放,導致那塊記憶體被白白佔據。也就是借了記憶體,卻忘記歸還。
Example :
int* p = new int(5);
當我們在 heap 配置一塊記憶體,後續卻沒有寫 :
delete p;
那這塊記憶體就不會被釋放,如果這種事情一值發生,當程式執行越久,吃掉的記憶體會越來越多。
拷貝物件
在 C++ 中,物件的拷貝分為「淺拷貝 Shallow Copy」和「深拷貝 Deep Copy」,選擇對的拷貝方式非常重要,可以避免記憶體相關的錯誤,並適當地管理資源。
淺拷貝 Shallow Copy : 快、風險高
- 只會複製各個物件成員的值;若成員中包含指標,則複製的是指標儲存的位址,而不是指標所指向的資料,因此不同物件會共享同一塊記憶體。
- 若不同物件的指標成員指向同一塊記憶體,則其中一個物件對資料的修改,會連帶影響其他物件。
- 這可能導致懸空指標 ( Dangling Pointer ) 和重複刪除 ( Double Deletion ) 等執行期錯誤。

深拷貝 Deep Copy : 慢、較安全
- 建立新物件時,不僅拷貝所有成員變數的值,還為動態配置的資料另外開一塊獨立的記憶體。
- 原始與拷貝後的物件會各自擁有獨立的資料,不會共享同一塊動態記憶體。
- 因為使用了自訂的拷貝建構子,所以其中一個物件的變動不會連帶影響另一個物件。

懸空指標 Dangling Pointer
指標還存著某個記憶體位置,但那塊記憶體其實已經失效。簡單來說就是,指標還在,但是它指向的物件已經死了。
Case 1 : delete 後指標沒有設定為 nullptr
int* p = new int(10); // 動態配置記憶體
delete p; // 釋放記憶體
cout << *p << endl; // 危險 : 讀取已經失效的記憶體,直接 crash
Case 2 : 使用區域變數的地址
變數 a 是區域變數,存在於 stack 空間。當區塊結束後,a 的生命週期也結束,那塊 stack 空間不再屬於它。最後 p 將指向一塊失效的記憶體位置。
int* p = nullptr;
{
int a = 1;
p = &a; // 儲存區域變數地址
}
cout << *p << endl; // 危險
重複刪除 Double Deletion
同一塊動態配置的記憶體被釋放兩次以上。這很危險,因為第一次 delete 之後,那塊記憶體就失效了,第二次會造成不預期的結果。
Case 1 : 兩個指標指向同一塊記憶體,卻都拿去釋放
int* p1 = new int(10);
int* p2 = p1; // p2 和 p1 指向同一塊記憶體
delete p1;
delete p2; // 出事 : double deletion
Case 2 : 淺拷貝導致兩個物件共用同一塊動態記憶體
在這個案例中,因為 Picture 類別沒有自己寫 copy constructor,預設拷貝會把成員值直接抄過去,所以 picture1.pixels 和 picture2.pixels 指向同一塊 heap 記憶體。
離開 main() 時,picture2 解構,pixels 被釋放。接著 picture1 解構,嘗試 delete 一塊失效的 heap 空間,直接出事。
int main() {
Picture picture1(10, 10);
Picture picture2 = picture1; // 出事
}
懸空指標和重複刪除的關係
這兩個常常一起出現,首先因為記憶體被釋放,某個指標變成懸空指標 ( dangling pointer ),再用那個懸空指標做 delete,最後導致重複刪除 ( double deletion )。所以這有點像連環車禍,兩個事件並非完全獨立。
int* p1 = new int(10);
int* p2 = p1;
delete p1; // 之後 p2 變成 dangling pointer
delete p2; // 再刪一次,就變成 double delete
傳遞參數間接導致的 Double Deletion
以值傳遞 ( call by value ) 方式傳入物件時,會產生一份新的物件拷貝;若類別未正確實作拷貝建構子,預設淺拷貝可能使原物件與參數物件共享同一塊動態記憶體,進而在解構時造成 double deletion。
void func1(Picture picture) {
cout << "func1" << endl;
}
int main() {
Picture picture(10, 10);
func1(picture); // 出事
}
使用 C++ 標準容器時的物件複製問題
在這個案例中,因為 push_back 會把 picture 複製一份到 vector 容器中,但 Picture 類別沒有自己寫一個 copy constructor,所以會使用預設淺拷貝。
這會導致 picture.pixels 和 vector 中物件的 pixels 指向同一塊動態記憶體。最後 main() 結束時,兩個物件各自執行 delete[] pixels 時,就會出事,造成 double deletion。
int main() {
vector<Picture> pictures;
Picture picture(10, 10);
pictures.push_back(picture); // 出事
}
如何避免懸空指標或重複刪除?
本文章舉例了非常多,跟物件有關,可能造成懸空指標、重複刪除造成的錯誤,那我們該怎麼避免呢?
- 重寫 copy constructor
- 重寫
operator= - 參數傳遞方式改為 call by reference
重寫 Copy Constructor
如果不重寫 copy constructor,編譯器預設只會做淺拷貝,把指標位置直接拷貝過去。所以重寫 copy constructor 的目的,就是讓新物件建立自己的資源,也就是深拷貝。
Picture p2 = p1;
Picture p3(p1);
重寫 Operator=
當一個已經存在的物件,被另一個同類物件指定內容時,會呼叫 operator=。如果 class 中有 const 成員,通常無法完成一般的指定操作,因此禁止使用 operator= 會比較安全。
Picture p1(10, 10);
Picture p2(20, 20);
p2 = p1; // 等同於 p2.operator=(p1)
// operator= 通常會 return *this,也就是 p2 自己的參考
// 所以 ( p2 = p1 ) 也是有回傳值的
在這個案例中,是把 p1 的內容指定給 p2。所以重寫 operator= 的目的,是優先處理 p2 原本的資源,在正確複製 p1 的資源,避免記憶體洩漏、淺拷貝或是 double deletion。
完成 Picture 類別,避免懸空指標或重複刪除
重寫 copy constructor 為深拷貝版本
為了深拷貝一份新的 Picture 物件,會直接複製原始物件的寬、高數值。而像素資料的部分,為了不與原始物件共用同一塊記憶體,需要動態配置另一個 heap 空間,並複製所有原始物件的像素資料到自己身上。
如此一來,原始與拷貝後的物件會各自擁有獨立的資料,不會共享同一塊動態記憶體,大大增加安全性。
Picture(const Picture& picture) : w(picture.w), h(picture.h) {
pixels = new int[w * h];
for (int i = 0; i < w * h; ++i) pixels[i] = picture.pixels[i];
}
重寫 operator= 為深拷貝版本
首先最重要的是,確定是不是自己指定給自己? 否則下面 delete[] pixels 會先把自己的資料刪掉,後面再從自己身上複製資料,就出事了。
Picture& operator=(const Picture& picture) {
if (this != &picture) { // 確定是不是自己指定給自己
delete[] pixels; // 刪除自己的資料
w = picture.w;
h = picture.h;
pixels = new int[w * h];
for (int i = 0; i < w * h; ++i) pixels[i] = picture.pixels[i];
}
return *this;
}
善用現代 C++ 的解決方案
在實務開發中,通常不建議手動使用 new 與 delete 管理記憶體,因為這類寫法容易導致淺拷貝、重複刪除等問題。現代 C++ 提供了更安全的做法,例如使用 std::vector 來管理動態陣列:
把 int* pixels 更換成 :
std::vector<int> pixels;
std::vector 會自動管理記憶體的配置與釋放,並且在複製物件時進行深拷貝,因此可以避免手動管理記憶體時常見的錯誤。
雖然現代 C++ 提供了 std::vector、智慧指標等工具來降低記憶體管理的風險,但這些工具只是封裝了底層機制,並沒有改變物件生命週期、所有權與拷貝行為等核心概念。若不了解這些基礎,即使使用現代寫法仍可能不小心產生錯誤。因此,學習傳統方式的目的,是為了理解資源管理的本質,更好地善用現代方法,而非鼓勵手動使用 new 與 delete。


