【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)