【C++ 筆記】動態記憶體(new / delete) - part 29

很感謝你點進來這篇文章。

你好,我並不是什麼 C++、程式語言的專家,所以本文若有些錯誤麻煩請各位鞭大力一點,我極需各位的指正及指導!!本系列文章的性質主要以詼諧的口吻,一派輕鬆的態度自學程式語言,如果你喜歡,麻煩留言說聲文章讚讚吧!

動態記憶體(Dynamic Memory)

什麼是動態記憶體?

當我們宣告一個變數的時候,編譯器會依據這個變數所屬的資料型態,自動配置其記憶體空間。這些資源都是配置於記憶體的堆疊區(stack),生命週期僅止於函數執行期間,當函數執行完成後就會自動清除。

另外,一旦配置後,就不能被刪除或更改他的大小。所以這時候動態記憶體就出現了。

在 C++ 中,記憶體分為兩部分(from 菜鳥教程):

  • 堆疊區(stack):在函數內部宣告的所有變數都將佔用堆積記憶體。
  • 堆積區(heap):程式中未使用的記憶體,在程式執行時可用於動態配置記憶體。

動態記憶體配置是在程式執行時配置記憶體的過程,這可以讓開發者在程式執行期間預留一些記憶體,依據開發者的需求去用它,然後再把記憶體給釋放以用於其他目的。

而上述所預留的「記憶體」就是所謂的「堆積區」記憶體。

動態記憶體的用處

  • 當你不確定一個陣列的大小時。
  • 可用來實作如 linked-list, trees 等這些資料結構。
  • 於需要高效率的記憶體管理的複雜程式當中。

動態記憶體的實作方式

C++ 提供兩種運算子,用於動態記憶體的配置與釋放:

  • 配置:new
  • 釋放:delete

new / delete 運算子

以下是 new 運算子的通用語法:

1
new data-type

data-type 可為任意內建資料型態,classstruct 這兩個自訂資料型態也可以。

先來看個簡單的小範例:

1
2
3
4
5
6
7
8
9
10
#include <iostream>

using namespace std;

int main() {
int* p = new int(10);
cout << "數值為: " << *p << endl;
delete p;
return 0;
}

Output:

1
數值為: 10

以上用 new 運算子配置一個內建的資料型態 int,值為 10,給指標 p。

為什麼 new 所配置的記憶體通常要用指標接收呢?如果不用 new,而是直接宣告變數並賦值,如 int x = 10;,這樣變數會配置在堆疊區(stack),而不是堆積區(heap),就不是動態記憶體配置了,自然失去使用 new 的意義。

另外 new int(10) 會回傳 int* 型態,你不用指標也不行。

最後要有個好習慣,就是寫 delete 手動釋放記憶體,避免記憶體洩漏。

陣列的動態記憶體配置

先來看範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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:

1
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])都配置到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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:

1
2
3
陣列內容 : 
0 0 0
0 1 2

那…三維陣列呢?

就是三重指標(int***),然後再跑雙層迴圈配置動態記憶體,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;

物件的動態記憶體配置

基本上跟簡單的內建資料型態沒啥差別。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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:

1
姓名 : LukeTseng, 年齡 : 18

執行時記憶體不夠了怎麼辦?

若堆積區中沒有足夠的記憶體可以去配置,還繼續用 new 去配置的話,就會拋出例外 std::bad_alloc,除非將 nothrownew 運算子一起使用,會回傳 nullptr

那在使用程式前,nothrownew 一起使用可以用來做個檢查,如:

1
2
3
4
int *p = new (nothrow) int;
if (!p) {
cout << "Memory allocation failed\n";
}

From GeeksForGeeks

跟動態記憶體有關的一些錯誤

記憶體洩漏(Memory Leaks)

這其實就是最後沒把記憶體釋放的結果,所以要養成好習慣,在程式結束前用 delete 釋放掉記憶體。

另外如果記憶體位址遺失,記憶體會一直保持配置狀態(與上述狀態相同)直到程式執行。

那有哪些狀況是記憶體位址遺失呢?

  1. 指標被覆蓋或重新指定
1
2
int* p = new int(10);
p = new int(20); // 原本指向 10 的記憶體無法再被釋放 -> 洩漏
  1. 指標變數離開作用域(Scope)
1
2
3
4
void foo() {
int* p = new int(30);
// 函式結束,p 被銷毀,記憶體遺失
}
  1. 動態陣列的部分元素位址遺失
1
2
3
int* arr = new int[5];
arr++; // 錯誤:位址不再指向起始位置,釋放時會錯誤
delete[] arr; // 未定義行為
  1. 指標遺失於容器中或函式回傳錯誤方式
1
2
3
4
int* create() {
int* p = new int(40);
return nullptr; // 原本的記憶體未回傳,無法釋放
}

C++ 11 有 std::unique_ptrstd::shared_ptr 等類別,稱為 smart pointer,可更好的協助動態記憶體配置,礙於篇幅,本篇暫不談。

迷途指標(Dangling Pointers)

在 C++ 中,迷途指標(Dangling Pointer)是指「指向無效記憶體位址的指標」。這種情況通常發生在指標曾指向一個合法的記憶體位址,但那塊記憶體已經被釋放或超出作用範圍,而指標本身還存在,造成錯誤的存取行為。

哪些是迷途指標的成因呢?

  1. 指標指向已被 delete 的記憶體
1
2
3
int* p = new int(10);
delete p; // 記憶體已釋放
*p = 5; // 未定義行為:p 是迷途指標
  1. 指標指向作用域外的區域變數
1
2
3
4
int* getPointer() {
int x = 20;
return &x; // x 在函式結束後即被銷毀,指標成為迷途指標
}
  1. 多個指標指向同一記憶體,卻重複釋放
1
2
3
4
5
int* p1 = new int(30);
int* p2 = p1;

delete p1;
*p2 = 10; // p2 是迷途指標

用個白話的例子來說明迷途指標:假設指標是一把鑰匙,記憶體是你的房子,然後有一天惠惠發神經用爆裂魔法把你家炸了(記憶體釋放),此時的你如同迷途的羔羊,站在你家門前,喔不,你已經沒門了XD,然後你手舉著鑰匙還想要開門,這就是迷途指標。

解決方式有兩種:

  1. 用 nullptr 初始化指標,釋放記憶體後再次指定為 nullptr。
  2. 用 smart pointer。(std::unique_ptrstd::shared_ptr

雙重釋放(Double Deletion)

顧名思義就是對同一塊動態配置的記憶體執行兩次 deletedelete[]

解決方式與迷途指標相同。

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 回傳的是指向堆積區的記憶體位址,必須用指標來接收,否則會失去動態記憶體配置的意義。
:::

單一變數的配置:

1
2
int* p = new int(10);
delete p;

陣列配置:

1
2
int* arr = new int[5];
delete[] arr;

二維陣列:

  • 配置
1
2
3
int** arr = new int*[rows];
for (int i = 0; i < rows; ++i)
arr[i] = new int[cols];
  • 釋放
1
2
3
for (int i = 0; i < rows; ++i)
delete[] arr[i];
delete[] arr;

三維陣列:

  • 配置
1
2
3
4
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];
  • 釋放
1
2
3
4
5
6
7
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
delete[] arr[i][j];
}
delete[] arr[i];
}
delete[] arr;

物件的配置:

1
2
Student* s = new Student("LukeTseng", 18);
delete s;

例外處理(記憶體不足):

1
2
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/freenew 必須配 deletemalloc() 必須配 free(),不可交叉使用。

解決方案

做法說明
指標初始化為 nullptr可避免未定義行為與迷途指標。
使用 smart pointerstd::unique_ptrshared_ptr 等更安全的管理方式。

參考資料

[Day 04] 用C++ 設計程式中的系統櫃:動態配置記憶體 | iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

new 與 delete

new and delete Operators in C++ For Dynamic Memory - GeeksforGeeks

bad_alloc in C++ - GeeksforGeeks

Memory leak in C++ - GeeksforGeeks

Dangling Pointers in C++ - GeeksforGeeks

C++ 动态内存 | 菜鸟教程