【C++ 筆記】動態記憶體(new / delete) - part 29 === 目錄(Table of Contents): [TOC] --- 很感謝你點進來這篇文章。 你好,我並不是什麼 C++、程式語言的專家,所以本文若有些錯誤麻煩請各位鞭大力一點,我極需各位的指正及指導!!本系列文章的性質主要以詼諧的口吻,一派輕鬆的態度自學程式語言,如果你喜歡,麻煩留言說聲文章讚讚吧! 動態記憶體(Dynamic Memory) --- ### 什麼是動態記憶體? 當我們宣告一個變數的時候,編譯器會依據這個變數所屬的資料型態,自動配置其記憶體空間。這些資源都是配置於記憶體的堆疊區(stack),生命週期僅止於函數執行期間,當函數執行完成後就會自動清除。 另外,一旦配置後,就不能被刪除或更改他的大小。所以這時候動態記憶體就出現了。 在 C++ 中,記憶體分為兩部分(from [菜鳥教程](https://www.runoob.com/cplusplus/cpp-dynamic-memory.html)): - 堆疊區(stack):在函數內部宣告的所有變數都將佔用堆積記憶體。 - **堆積區(heap):程式中未使用的記憶體,在程式執行時可用於動態配置記憶體。** 動態記憶體配置是在程式執行時配置記憶體的過程,這可以讓開發者在程式執行期間預留一些記憶體,依據開發者的需求去用它,然後再把記憶體給釋放以用於其他目的。 而上述所預留的「記憶體」就是所謂的「**堆積區**」記憶體。 ### 動態記憶體的用處 - 當你不確定一個陣列的大小時。 - 可用來實作如 linked-list, trees 等這些資料結構。 - 於需要高效率的記憶體管理的複雜程式當中。 ### 動態記憶體的實作方式 C++ 提供兩種運算子,用於動態記憶體的配置與釋放: - 配置:`new`。 - 釋放:`delete`。 `new` / `delete` 運算子 --- 以下是 `new` 運算子的通用語法: ```cpp new data-type ``` data-type 可為任意內建資料型態,`class`、`struct` 這兩個自訂資料型態也可以。 先來看個簡單的小範例: ```cpp= #include <iostream> using namespace std; int main() { int* p = new int(10); cout << "數值為: " << *p << endl; delete p; return 0; } ``` Output: ``` 數值為: 10 ``` 以上用 new 運算子配置一個內建的資料型態 int,值為 10,給指標 p。 為什麼 `new` 所配置的記憶體通常要用指標接收呢?如果不用 `new`,而是直接宣告變數並賦值,如 `int x = 10;`,這樣變數會配置在堆疊區(stack),而不是堆積區(heap),就不是動態記憶體配置了,自然失去使用 `new` 的意義。 另外 `new int(10)` 會回傳 `int*` 型態,你不用指標也不行。 最後要有個好習慣,就是寫 `delete` 手動釋放記憶體,避免記憶體洩漏。 ### 陣列的動態記憶體配置 先來看範例: ```cpp= #include <iostream> using namespace std; int main(){ int *arr = new int[5]; for (int i = 0; i < 5; ++i){ arr[i] = i * 2; } for (int i = 0; i < 5; ++i){ cout << arr[i] << " "; } cout << endl; delete[] arr; return 0; } ``` Output: ``` 0 2 4 6 8 ``` 要為陣列做動態記憶體配置,需要將 `new int(5)` 寫成 `new int[5]`,表示要對陣列做動態記憶體配置。 因此在釋放記憶體的時候,也要寫成 `delete[]`,避免未定義行為。 ### 二維陣列的動態記憶體配置 二維陣列的動態記憶體配置就複雜了一點,`int** arr = new int*[rows];` 就用到了雙重指標(指向指標的指標:`int**`),讓每一列(`arr[i][j]` 的 `[i]`)都是 `int*` 型態。 之後還要再配置一次,就是下面的 for loop,讓每一行(`arr[i][j]` 的 `[j]`)都配置到。 ```cpp= #include <iostream> using namespace std; int main(){ int rows = 2; int cols = 3; int** arr = new int*[rows]; for (int i = 0; i < rows; ++i){ arr[i] = new int [cols]; } for (int i = 0; i < rows; ++i){ for (int j = 0; j < cols; ++j){ arr[i][j] = i * j; } } cout << "陣列內容 : " << endl; for (int i = 0; i < rows; ++i){ for (int j = 0; j < cols; ++j){ cout << arr[i][j] << " "; } cout << endl; } for (int i = 0; i < rows; ++i){ delete[] arr[i]; } delete[] arr; return 0; } ``` Output: ``` 陣列內容 : 0 0 0 0 1 2 ``` ### 那...三維陣列呢? 就是三重指標(`int***`),然後再跑雙層迴圈配置動態記憶體,如下所示: ```cpp= int m = 5; int n = 4; int k = 3; // 配置 int*** arr = new int **[m]; for (int i = 0; i < m; ++i){ arr[i] = new int *[n]; for (int j = 0; j < n; ++j){ arr[i][j] = new int [k]; } } // 釋放 for (int i = 0; i < m; ++i){ for (int j = 0; j < n; ++j){ delete[] arr[i][j]; } delete[] arr[i]; } delete[] arr; ``` ### 物件的動態記憶體配置 基本上跟簡單的內建資料型態沒啥差別。 ```cpp= #include <iostream> using namespace std; class Student{ public: string name; int age; Student (string n, int a) : name(n), age(a) {} void display(){ cout << "姓名 : " << name << ", 年齡 : " << age << endl; } }; int main(){ Student* s = new Student("LukeTseng", 18); s->display(); delete s; return 0; } ``` Output: ``` 姓名 : LukeTseng, 年齡 : 18 ``` 執行時記憶體不夠了怎麼辦? --- 若堆積區中沒有足夠的記憶體可以去配置,還繼續用 `new` 去配置的話,就會拋出例外 `std::bad_alloc`,除非將 `nothrow` 與 `new` 運算子一起使用,會回傳 `nullptr`。 那在使用程式前,`nothrow` 跟 `new` 一起使用可以用來做個檢查,如: ```cpp= int *p = new (nothrow) int; if (!p) { cout << "Memory allocation failed\n"; } ``` From [GeeksForGeeks](https://www.geeksforgeeks.org/new-and-delete-operators-in-cpp-for-dynamic-memory/)。 跟動態記憶體有關的一些錯誤 --- ### 記憶體洩漏(Memory Leaks) 這其實就是最後沒把記憶體釋放的結果,所以要養成好習慣,在程式結束前用 delete 釋放掉記憶體。 另外如果記憶體位址遺失,記憶體會一直保持配置狀態(與上述狀態相同)直到程式執行。 那有哪些狀況是記憶體位址遺失呢? 1. 指標被覆蓋或重新指定 ```cpp= int* p = new int(10); p = new int(20); // 原本指向 10 的記憶體無法再被釋放 -> 洩漏 ``` 2. 指標變數離開作用域(Scope) ```cpp= void foo() { int* p = new int(30); // 函式結束,p 被銷毀,記憶體遺失 } ``` 3. 動態陣列的部分元素位址遺失 ```cpp= int* arr = new int[5]; arr++; // 錯誤:位址不再指向起始位置,釋放時會錯誤 delete[] arr; // 未定義行為 ``` 4. 指標遺失於容器中或函式回傳錯誤方式 ```cpp= int* create() { int* p = new int(40); return nullptr; // 原本的記憶體未回傳,無法釋放 } ``` C++ 11 有 `std::unique_ptr` 、 `std::shared_ptr` 等類別,稱為 smart pointer,可更好的協助動態記憶體配置,礙於篇幅,本篇暫不談。 ### 迷途指標(Dangling Pointers) 在 C++ 中,迷途指標(Dangling Pointer)是指「**指向無效記憶體位址的指標**」。這種情況通常發生在指標曾指向一個合法的記憶體位址,但那塊記憶體已經被釋放或超出作用範圍,而指標本身還存在,造成錯誤的存取行為。 哪些是迷途指標的成因呢? 1. 指標指向已被 delete 的記憶體 ```cpp= int* p = new int(10); delete p; // 記憶體已釋放 *p = 5; // 未定義行為:p 是迷途指標 ``` 2. 指標指向作用域外的區域變數 ```cpp= int* getPointer() { int x = 20; return &x; // x 在函式結束後即被銷毀,指標成為迷途指標 } ``` 3. 多個指標指向同一記憶體,卻重複釋放 ```cpp= int* p1 = new int(30); int* p2 = p1; delete p1; *p2 = 10; // p2 是迷途指標 ``` 用個白話的例子來說明迷途指標:假設指標是一把鑰匙,記憶體是你的房子,然後有一天惠惠發神經用爆裂魔法把你家炸了(記憶體釋放),此時的你如同迷途的羔羊,站在你家門前,喔不,~~你已經沒門了XD~~,然後你手舉著鑰匙還想要開門,這就是迷途指標。 解決方式有兩種: 1. 用 nullptr 初始化指標,釋放記憶體後再次指定為 nullptr。 2. 用 smart pointer。(`std::unique_ptr` 、 `std::shared_ptr`) ### 雙重釋放(Double Deletion) 顧名思義就是對同一塊動態配置的記憶體執行兩次 `delete` 或 `delete[]`。 解決方式與迷途指標相同。 ### new / delete與 malloc() / free() 混用 `malloc()`、`free()` 是 C-style 的動態記憶體配置與釋放,只能選擇 C++ style 或 C-style 一個使用,因為這兩個都不相容。 另外 C++ 也有支援上述兩個函數,但用 new 跟 delete 會比那兩個函數好、又安全。 總結 --- C++ 記憶體分成兩大塊區域: | 區域 | 說明 | | --------- | -------------------------- | | 堆疊區 stack | 函式內部變數,生命週期短,編譯器自動配置與釋放。 | | 堆積區 heap | 執行期間可手動配置與釋放的記憶體空間,用於動態記憶體。 | new / delete: | 操作 | 功能 | | -------- | ----------------------- | | `new` | 在堆積區配置記憶體,回傳指標。 | | `delete` | 釋放 `new` 配置的記憶體,避免記憶體洩漏(所以記得每次程式結束前要釋放記憶體)。 | :::info 為什麼 `new` 要搭配指標使用? 因為 `new` 回傳的是指向堆積區的記憶體位址,必須用指標來接收,否則會失去動態記憶體配置的意義。 ::: 單一變數的配置: ```cpp= int* p = new int(10); delete p; ``` 陣列配置: ```cpp= int* arr = new int[5]; delete[] arr; ``` 二維陣列: - 配置 ```cpp= int** arr = new int*[rows]; for (int i = 0; i < rows; ++i) arr[i] = new int[cols]; ``` - 釋放 ```cpp= for (int i = 0; i < rows; ++i) delete[] arr[i]; delete[] arr; ``` 三維陣列: - 配置 ```cpp= int*** arr = new int**[m]; for (int i = 0; i < m; ++i) for (int j = 0; j < n; ++j) arr[i][j] = new int[k]; ``` - 釋放 ```cpp= for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { delete[] arr[i][j]; } delete[] arr[i]; } delete[] arr; ``` 物件的配置: ```cpp= Student* s = new Student("LukeTseng", 18); delete s; ``` 例外處理(記憶體不足): ```cpp= int* p = new (nothrow) int; if (!p) cout << "配置失敗"; ``` ### 常見錯誤 | 問題類型 | 說例 | | ----------- | ------------------------------------------------------- | | 記憶體洩漏 | 忘記釋放、或位址遺失:`p = new int(20); // 沒釋放舊的`。 | | 迷途指標 | 使用已釋放或作用域外的指標:`int* p = new int; delete p; *p = 5;`。 | | 雙重釋放 | 對同一記憶體重複 delete:`delete p1; delete p2;`(若 p1 == p2)。 | | 混用 new/free | `new` 必須配 `delete`,`malloc()` 必須配 `free()`,不可交叉使用。 | ### 解決方案 | 做法 | 說明 | | ---------------- | ---------------------------------------- | | 指標初始化為 `nullptr` | 可避免未定義行為與迷途指標。 | | 使用 smart pointer | `std::unique_ptr`、`shared_ptr` 等更安全的管理方式。 | 參考資料 --- [[Day 04] 用C++ 設計程式中的系統櫃:動態配置記憶體 | iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天](https://ithelp.ithome.com.tw/m/articles/10287425) [new 與 delete](https://openhome.cc/Gossip/CppGossip/newDelete.html) [new and delete Operators in C++ For Dynamic Memory - GeeksforGeeks](https://www.geeksforgeeks.org/new-and-delete-operators-in-cpp-for-dynamic-memory/) [bad_alloc in C++ - GeeksforGeeks](https://www.geeksforgeeks.org/cpp/bad_alloc-in-cpp/) [Memory leak in C++ - GeeksforGeeks](https://www.geeksforgeeks.org/cpp/memory-leak-in-c-and-how-to-avoid-it/) [Dangling Pointers in C++ - GeeksforGeeks](https://www.geeksforgeeks.org/cpp/dangling-pointers-in-cpp/) [C++ 动态内存 | 菜鸟教程](https://www.runoob.com/cplusplus/cpp-dynamic-memory.html)