Home
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

4

Local AI

--

Uedu Code

--

Taipei AQI 57 · Taichung AQI 74 · Tainan AQI 63 · Kaohsiung AQI 64
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 14

動態記憶體

new 與 delete、陣列配置、記憶體洩漏與懸空指標
Interaction實作 25 minute(s)

Learning Objectives

  • 分辨堆疊(stack)與堆積(heap):誰負責配置、什麼時候釋放
  • 用 new 配置單一變數與物件、用 delete 釋放,說明建構子與解構子何時被呼叫
  • 用 new[] 配置執行時才決定大小的陣列,並用 delete[] 配對釋放
  • 辨認記憶體洩漏、懸空指標與重複 delete,並用 delete 後設成 nullptr 降低風險
  • 說明現代 C++ 為什麼優先使用 vector 與智慧指標

堆疊與堆積

到目前為止,你宣告的變數都是區域變數:函式開始時自動配置,函式結束時自動消失。它們放在記憶體中叫做堆疊(stack)的區域, 由編譯器自動管理,又快又方便,但有兩個限制:

  • 壽命跟著函式:函式一結束就消失,不能把它的位址交給別人長期使用(上一課的懸空指標就是這樣來的)
  • 大小要事先決定:陣列 int a[100]; 的大小必須在編譯時就確定,而且堆疊空間有限(Linux 預設通常是 8 MB)

另一塊區域叫堆積(heap,也叫自由儲存區 free store)。在堆積上配置的記憶體,由你決定什麼時候配置、什麼時候釋放:

堆疊(stack) 堆積(heap)
怎麼配置 宣告區域變數,自動 new,手動
什麼時候釋放 離開所在的區塊,自動 delete,手動
大小 編譯時決定,空間較小 執行時決定,空間大
位址(本課動畫) 0x7ffd… 0x55a3c6…
int* p = new int(25);   // 在堆積配置一個 int,初始值 25,回傳它的位址
delete p;               // 用完還回去

自由的代價是責任:每個 new 都要有一個對應的 delete,忘了就會洩漏,還太早或還兩次都會出錯。 本課動畫的右側有一欄堆積,每次 new 會出現一個區塊,delete 之後標示為已釋放;程式結束時還沒釋放的區塊會被標示為記憶體洩漏。

new 與 delete

比較 local(堆疊)和 p、d 指向的區塊(堆積)的位址開頭。注意第 13 行 delete p 之後,區塊被釋放了,但 p 的值還是舊位址,要到第 15 行才變成 nullptr。

執行時才決定大小的陣列

陣列的大小必須是編譯時就知道的常數。如果要依使用者輸入決定大小,就在堆積上配置:

int n;
cin >> n;
int* arr = new int[n];    // n 個 int,連續排列
arr[0] = 5;               // 用法和一般陣列一樣
delete[] arr;             // 注意是 delete[]

new 配 delete,new[] 配 delete[],不能混用:用 delete 釋放 new[] 配置的陣列,或反過來,都是未定義行為。

g++ 其實接受 int arr[n]; 這種寫法(可變長度陣列),但這是 g++ 的擴充功能,不是標準 C++,其他編譯器不一定支援; 陣列很大時還可能用光堆疊空間。

new[] 與 delete[]

輸入區已經準備好:先輸入 4,再輸入四個分數。第 9 行執行後,堆積欄會出現一個能放 4 個 int 的區塊,迴圈讀入分數時區塊裡的值會一格一格填上。

想一想:new 與 delete

1.

下面的程式會印出什麼?

int* p = new int(5);
int* q = p;
*q += 2;
cout << *p;
delete p;
2.

double* data = new double[100]; 用完之後,正確的釋放方式是哪一個?

3.

下面的程式執行時會怎樣?

int* p = new int(1);
delete p;
delete p;
cout << "done";
第 1 次作答

記憶體洩漏

new 配置的區塊不會自己消失:就算存放位址的指標變數已經不見了,區塊還留在堆積裡。 如果沒有任何指標記得它的位址,就再也沒有辦法 delete 它,這塊記憶體在程式結束前都浪費掉了,這叫做記憶體洩漏(memory leak)。

常見的原因:

  • 忘了寫 delete
  • 指標被改成指向別的地方,原本的位址弄丟了
  • 函式在 delete 之前就 return 或丟出例外

一次洩漏幾個位元組看不出影響,但如果發生在迴圈裡、或程式要連續執行好幾天,記憶體用量會一直上升。

記憶體洩漏是怎麼發生的

盯著右邊的堆積欄:makeCounter 結束時區域變數 c 消失了,但它配置的區塊留了下來。第 16 行之後,存著 7 的區塊已經沒有任何指標指向它。播到最後一步,堆積欄裡剩下的區塊就是洩漏的記憶體。

懸空指標與重複 delete

delete p; 釋放的是 p 指向的區塊,p 這個變數本身不會變,仍然存著舊位址。之後的錯誤有兩種:

錯誤 例子 結果
使用已釋放的記憶體(use after free) delete p; cout << *p; 未定義行為:可能讀到怪值、改壞別的資料或當掉
重複 delete(double free) delete p; delete p; 未定義行為:通常當掉

delete 之後立刻設成 nullptr 是好習慣:之後可以用 if (p != nullptr) 檢查,而且 delete nullptr; 是安全的,什麼都不做。

但這招只保護得了被設成 nullptr 的那一個指標。如果還有別的指標複製了同一個位址,它們全都成了懸空指標:

另一個指標還記得舊位址

p 和 q 指向同一個區塊。delete p 之後 p 被設成 nullptr,q 卻沒有人更新。播到最後一步,看程式停下來的原因。

在堆積上建立物件

new 也可以建立類別的物件,這時會做兩件事:配置記憶體,然後呼叫建構子;delete 則是先呼叫解構子,再釋放記憶體。

Player* hero = new Player("勇者", 100);
hero->hp -= 30;      // 透過指標存取成員用 ->,等於 (*hero).hp
delete hero;         // 呼叫 ~Player(),再釋放

區域物件離開大括號時會自動解構;用 new 建立的物件不會,一定要 delete 才會呼叫解構子。

new 呼叫建構子,delete 呼叫解構子

hero 指向堆積上的物件,guard 是堆疊上的區域物件。注意 guard 在第 24 行離開大括號時自動解構,hero 指向的物件要等到第 25 行 delete 才解構。

想一想:洩漏、懸空與物件

1.

下面的程式執行完之後,有幾塊記憶體洩漏?

int* a = new int(1);
int* b = new int(2);
a = b;
delete a;
2.

下面的程式會印出什麼?

struct T {
    char c;
    T(char x) : c(x) { cout << c; }
    ~T() { cout << c; }
};

int main() {
    T* a = new T('A');
    T b('B');
    delete a;
}
3.

要存放「執行時才知道有幾筆」的成績,現代 C++ 最建議哪一種寫法?

第 1 次作答

現代 C++:讓物件替你管理記憶體

手動 new/delete 很容易出錯:漏掉 delete、delete 兩次、delete 之前就丟出例外。現代 C++ 的做法是把記憶體交給物件管理, 物件解構時自動釋放(這個原則叫 RAII,resource acquisition is initialization)。

動態陣列用 vector:

vector<int> scores(n);   // 在堆積上配置 n 個 int
// 不需要 delete,scores 解構時自動釋放

單一物件用智慧指標(標頭檔 <memory>):

#include <memory>

unique_ptr<Player> hero = make_unique<Player>("勇者", 100);  // 只有一個擁有者
hero->hp -= 30;
// 不需要 delete:hero 離開範圍時自動 delete

shared_ptr<Player> a = make_shared<Player>("守衛", 50);     // 可以有多個擁有者
shared_ptr<Player> b = a;   // 最後一個擁有者消失時才 delete

unique_ptr、shared_ptr 是 C++11 加入的,make_unique 是 C++14。本課的直譯器不支援智慧指標,所以只在這裡介紹; 在自己的電腦上用 g++ 編譯可以正常使用。

理解 new/delete 仍然重要:你會在舊程式碼、課本與面試題裡看到它們,vector 和智慧指標在內部也是這樣運作的。

動手寫:反向印出輸入的數字

程式先讀入數量 n,再讀入 n 個整數。請用 new int[n] 配置陣列存放,反向印出,最後用 delete[] 釋放。輸入區已經準備好 5 和 3 8 1 9 4(讀入時輸入的內容會顯示在輸出裡)。

你的輸出
預期輸出

重點回顧

  • 區域變數放在堆疊,自動配置與釋放;new 在堆積配置,必須自己 delete
  • new T(值) 配置並初始化,回傳位址;new 物件會呼叫建構子,delete 會呼叫解構子
  • new[] 配置執行時才決定大小的陣列,一定要用 delete[] 釋放;new 配 delete、new[] 配 delete[]
  • 沒有 delete、或弄丟位址,就是記憶體洩漏;delete 之後還使用是懸空指標;delete 兩次是 double free
  • delete 之後把指標設成 nullptr,但其他複製了同一位址的指標仍然是懸空的
  • 現代 C++ 優先用 vector 與智慧指標(unique_ptr、shared_ptr),讓物件在解構時自動釋放記憶體
這一課完成了!
Next lesson:遞迴
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。