x=42pheap
PREREQ P4

陣列、指標動態記憶體

先備知識 · 指標與生命週期
&x|*p|NULL|a[i] == *(a+i)|new / delete|堆疊 vs 堆積
向下捲動開始互動
📌 本頁使用方式(先備知識 · 指標與生命週期)

① 這頁從語法開始建立陣列、位址、指標與動態記憶體的完整觀念。已經熟的可以直接跳過。 ② 每節都附一個「這在資料結構課哪裡會用到」的小方框——那才是你該記住的部分。 ③ 讀完做 自我檢測,再翻 關鍵詞彙卡
先確認能分辨指標與所指物件,再練習配置與釋放。

CONTENTS · 內容目錄
PROLOGUE · 開場

整套先備頁裡,這一頁最該讀

指標把物件的位置變成可保存、比較與傳遞的值,也讓程式能表達「目前沒有對象」與執行期才建立的物件:

一般情境newnullptr指標扮演的角色
可選的裝置設定可用未設定裝置以指標參照可能不存在的設定物件
目前選取的文件可用未選取指標可在不同文件物件之間改指
大型影像緩衝區動態配置失效後清空大小在執行時決定,owner 負責釋放
函式的可選輸出依介面沒有結果呼叫端先檢查,再透過指標取得物件
共同要求明確 owner先檢查所有位址都必須指向仍存活且型別正確的物件

一般變數讓你直接使用值;指標則把值所在的位址也變成可保存、比較與傳遞的資料。這份控制能力伴隨一項責任:使用位址前,必須確認目標仍然存在。

📌 本頁定位

先依序讀懂 &*nullptr 與生命週期,再用 Device/Settings 的具名關聯整合這些語法。

PART 01 · 位址

每個變數都住在一個門牌號碼

#include <iostream>
int main() {
    int value = 42;
    int *address = &value;
    std::cout << "值 = " << *address << '\n';
}

變數不是憑空存在的,它佔一塊記憶體。&x 就是問「x 住在哪」,sizeof(x) 是問「它佔幾格」。

每個變數的門牌與大小(真的跑出來的)
#include <iostream> using namespace std; int main() { int x = 42; double d = 3.14; char c = 'A'; cout << "x 的值 = " << x << ",位址 = " << &x << ",佔 " << sizeof(x) << " bytes" << endl; cout << "d 的值 = " << d << ",位址 = " << &d << ",佔 " << sizeof(d) << " bytes" << endl; cout << "c 的值 = " << c << ",位址 = " << (void*)&c << ",佔 " << sizeof(c) << " bytes" << endl; return 0; }
預期輸出
x  的值 = 42,位址 = 0x7ffe91aff68c,佔 4 bytes
d  的值 = 3.14,位址 = 0x7ffe91aff690,佔 8 bytes
c  的值 = A,位址 = 0x7ffe91aff68b,佔 1 bytes

三個觀察:① 位址是十六進位的一長串,每次執行都不一樣(作業系統每次配的位置不同)。② 不同型別佔的大小不同:int 4 bytes、double 8、char 1。③ char 的位址要轉成 (void*) 才印得出數字,否則 cout 會把它當字串開頭。

按「開始」,一步一步看指標怎麼指到變數身上。
速度
程式碼 CODE
int x = 42; int *p = &x; cout << *p; *p = 99; cout << x;
三個符號 KEY

int *p:宣告 p 是「指向 int 的指標」
&x:取出 x 的位址
*p:順著 p 存的位址走進去

① 宣告之後 int x = 25; int *p = &x; 記憶體 x(一般變數) 25 位址 0x1000 p(指標) 0x1000 位址 0x1008 p 存的是 x 的門牌 箭頭=「這串位址所指的地方」 ② 執行之後 *p = 50; 記憶體 x 被改了 50 位址 0x1000(沒變) p 本身沒變 0x1000 位址 0x1008 順著箭頭走進去 改到的是 x 本人,不是副本

圖 1:x 盒裡裝的是值 25,p 盒裡裝的是「x 的位址」——所以 *p = 50; 改的是箭頭那一頭的 x,不是 p 自己。上面的動畫演逐步,這張圖給你前後對照。

PART 02 · 指標

int *p = &x;:三個符號,三件事

初學最容易混的是:* 在宣告時和在使用時意思不一樣。

寫法出現在哪意思
int *p;宣告p 是一個指標,指向 int
p = &x;使用把 x 的位址存進 p
*p使用解參考:p 指向的那個東西(就是 x 本人)
*p = 99;使用透過 p 改動 x
p使用指標本身的值,也就是一個位址
同一段程式把五件事都做一遍
#include <iostream> using namespace std; int main() { int x = 42; int *p = &x; // p 存的是 x 的位址 cout << "x = " << x << endl; cout << "&x = " << &x << endl; cout << "p = " << p << " <- p 存的就是 x 的位址" << endl; cout << "*p = " << *p << " <- 順著位址走進去,看到 x 的值" << endl; *p = 99; // 透過指標改動 x cout << "把 *p 改成 99 之後,x = " << x << endl; cout << "指標本身也佔空間: sizeof(p) = " << sizeof(p) << " bytes" << endl; return 0; }
預期輸出
x  = 42
&x = 0x7ffeec7bfa4c
p  = 0x7ffeec7bfa4c   <- p 存的就是 x 的位址
*p = 42   <- 順著位址走進去,看到 x 的值
把 *p 改成 99 之後,x = 99
指標本身也佔空間: sizeof(p) = 8 bytes

注意倒數第二行:透過 *p 改值,x 真的變了——兩個寫法存取同一個物件。範例環境的指標是 8 bytes;實際大小由平台決定,應以該次執行的 sizeof(p) 為準。

💡 為什麼要有指標

兩個常見理由:① 避免複製目標物件——傳遞位址即可讓函式找到原物件(見 P3)。② 讓結構在執行時長大——項目數量事先未知時,可動態建立物件,而 new 回傳的就是指標。

參考:另一個名字,不是另一個變數 —— 跟指標分清楚

C++ 還有一個長得像親戚的東西:參考(reference,有些書譯作「參照」)。 int &r = x; 讀作「r 是 x 的參考」—— 它不配置新的記憶體、也不存位址, 它就是 x 的另一個名字。若印出 &r == &x,會看到兩者位址相同;sizeof(r) 也會得到所指型別的大小, 因為 r 根本沒有自己的身體。兩者常被混為一談,但個性完全不同:

參考 int &r = x;指標 int *p = &x;
它是什麼x 的別名,編譯器層級的化名一個存位址的變數,有自己的記憶體
宣告時可以不初始化嗎不行,宣告當下就得綁定對象可以(但未初始化就是野指標,見下)
可以「不指向任何東西」嗎不行,沒有「空參考」這種東西可以,p = nullptr;
可以中途換對象嗎不行,終身只認第一個可以,p = &y; 隨時改指
實作是否另有儲存語言只保證別名語意;sizeof(r) 量到所指型別,不能據此判定實作指標有自己的儲存,sizeof(p) 由平台資料模型決定
取用時要加符號嗎不用,寫 r 就是 x 本人要,*p 才走得進去
本套頁面的典型用途函式參數與回傳(P3T&const T&可選的 settings、目前選取的物件、new 的回傳值
✅ 怎麼選:用語意決定,不是用喜好

只是想讓函式碰到本人、或省下複製:用參考,呼叫端乾淨、也不必防 nullptr。需要「可能沒有對象」或「會換對象」的語意,例如裝置的 settings 尚未建立,或 selected 要改指另一個物件,就需要使用指標。

隨堂 · 參考 vs 指標

下列哪件事是指標做得到、參考做不到的?

(A) 讓函式改到呼叫端的變數
(B) 中途改成指向另一個變數
(C) 避免複製整個物件
(D) 只能讀取、不能修改目標
PART 03 · 空指標

nullptr:指向「哪裡都不是」 空目標的標準寫法

nullptr 代表「這個指標目前沒有指向任何物件」。它可表示一項可選關聯目前尚未設定。

逐一檢查可選的整數

這裡先用一個固定陣列保存四個指標:int* choices[4] 中,int* 是每格的型別,choices 是名稱,[4] 是格數,大括號依序給初值。範圍 for 每次取出一格的指標,再檢查是否為 nullptr;下一節會完整拆解陣列與索引。

#include <iostream> using namespace std; int main() { int first = 31, third = 17, fourth = 93; int *choices[4] = { &first, nullptr, &third, &fourth }; for (int *current : choices) { if (current != nullptr) cout << *current << " "; } cout << endl; return 0; }
預期輸出
31 17 93

第二個元素是 nullptr,表示該位置沒有整數可讀。程式先檢查再解參考 *current,因此會安全跳過空項目。

⚠️ 不可解參考 nullptr

*currentcurrentnullptr 時是未定義行為;可能崩潰,也可能呈現其他結果。必須先檢查指標再解參考。

✅ 現代 C++ 對照

C++11 之後的 nullptr 有專用型別,能避免舊式整數空值在多載中造成歧義;本頁一律使用它。

🔗 可選關聯的共同規則

裝置設定、目前工作或快取結果都可能暫時不存在。原始指標用 nullptr 表達「沒有對象」時,每次解參考前都必須先檢查。

PART 04 · 陣列

a[i] 其實就是 *(a + i)

陣列本身有陣列型別,不是指標變數。它在許多運算式中會轉換成指向首元素的指標;此時 a[i] 等價於 *(a + i)。sizeof(a) 與 &a 等情況不作這種轉換,因此不能一律把陣列名稱當成位址。

三種寫法印出同一個東西
#include <iostream> using namespace std; int main() { int a[5] = {10, 20, 30, 40, 50}; cout << "a = " << a << " <- 此處 a 轉換成首元素指標" << endl; cout << "&a[0] = " << &a[0] << " <- 一模一樣" << endl; cout << endl; for (int i = 0; i < 3; i++) { cout << "i=" << i << " a[i]=" << a[i] << " *(a+i)=" << *(a + i) << " 位址=" << (a + i) << " 距離首位 " << (char*)(a + i) - (char*)a << " bytes" << endl; } return 0; }
預期輸出
a       = 0x7ffef812b1b0  <- 此處 a 轉換成首元素指標
&a[0]   = 0x7ffef812b1b0  <- 一模一樣

i=0  a[i]=10  *(a+i)=10  位址=0x7ffef812b1b0  距離首位 0 bytes
i=1  a[i]=20  *(a+i)=20  位址=0x7ffef812b1b4  距離首位 4 bytes
i=2  a[i]=30  *(a+i)=30  位址=0x7ffef812b1b8  距離首位 8 bytes

看最後一欄:每跳一格,位址就前進 4 bytes——剛好是一個 int 的大小。指標算術會自動乘上型別大小,所以 a + 1 不是「位址加 1」,是「往後一個元素」。這也解釋了為什麼索引從 0 開始:a[0] 就是「往後跳 0 格」,也就是原地。

索引 i 0
拉動滑桿,看三種寫法永遠指向同一格。
為什麼要懂 WHY

二維陣列採 row-major 佈局時,matrix[i][j] 位於第 i * 欄數 + j 個元素。這個公式正是由一維指標算術延伸而來。

🔗 從一維位址到二維排列

指標算術說明低階陣列的位址計算,也能推導二維陣列的 row-major 位置公式;實務上仍應避免讓指標越過陣列合法範圍。

PART 05 · 退化

陣列一進函式,就變成一個指標

同一個陣列,sizeof 卻不一樣
#include <iostream> using namespace std; void f(int arr[]) { // 看起來像陣列,其實是 int* cout << "在函式裡 sizeof(arr) = " << sizeof(arr) << " bytes(這是指標的大小)" << endl; } int main() { int a[5] = {10, 20, 30, 40, 50}; cout << "在 main 裡 sizeof(a) = " << sizeof(a) << " bytes(5 個 int)" << endl; f(a); return 0; }
預期輸出
在 main 裡 sizeof(a) = 20 bytes(5 個 int)
在函式裡 sizeof(arr) = 8 bytes(這是指標的大小)

void f(int arr[]) 寫在參數列時會調整成 int*。所以函式裡量到的是指標大小;範例平台顯示 8 bytes,而呼叫端五元素陣列顯示 20 bytes。這叫陣列退化,也說明長度必須另外傳入。

⚠️ 所以陣列傳進函式一定要另外傳長度

因為函式根本無從得知長度。void printAll(int arr[], int n) 這種簽章不是囉嗦,是必要。vector 就沒這個問題——它自己記得 size(),這也是 P5 要講的第一個好處。

備註:void (*tests[])(int) 是什麼

批次執行多個測試函式時,可能遇到這種宣告: void (*tests[])(int) = {test1, test2, test3, test4};。 讀法由內往外:tests 是一個陣列,每個元素是「收一個 int、不回傳東西的函式指標」。 之後 tests[k](n) 就是「呼叫第 k 個測試函式」——迴圈跑四個函式不用寫四段重複的碼。 能這樣塞是因為函式名跟陣列名一樣,會退化成指標(正是本節講的那件事)。 這是函式指標與陣列退化的延伸例子,先能由內往外讀懂宣告即可。

PART 06 · 兩塊記憶體

堆疊與堆積:生命週期完全不同

堆疊 stack堆積 heap
怎麼配置宣告區域變數就有要自己寫 new
怎麼釋放離開範圍自動消失要自己寫 delete
可用空間通常較小,由執行環境決定通常較大,同樣受系統資源限制
元素數量何時決定區域物件的型別與大小固定;進入作用域時建立new T[n]n 可在執行時決定
本套頁面用在哪迴圈變數、暫存值執行期配置的設定、緩衝區與其他物件

函式呼叫太深可能耗盡堆疊new 建立的物件若失去可用指標而未釋放,則會造成堆積上的記憶體洩漏。

PART 07 · 借與還

newdelete 配置與釋放必須配對

借了要還,而且形式要配對
#include <iostream> using namespace std; int main() { int onStack = 42; // 堆疊:離開範圍自動消失 int *onHeap = new int(42); // 堆積:你不 delete 就一直在 cout << "堆疊變數位址 = " << &onStack << endl; cout << "堆積變數位址 = " << onHeap << endl; cout << "兩者的值都是 " << onStack << " / " << *onHeap << endl; int *arr = new int[5]; // 配一整排 for (int i = 0; i < 5; i++) arr[i] = i * i; cout << "arr = "; for (int i = 0; i < 5; i++) cout << arr[i] << " "; cout << endl; delete onHeap; // 單一個用 delete delete[] arr; // 一整排用 delete[] cout << "都還回去了。" << endl; return 0; }
預期輸出
堆疊變數位址 = 0x7fff825e572c
堆積變數位址 = 0x6146265982b0
兩者的值都是 42 / 42
arr = 0 1 4 9 16
都還回去了。

注意兩個位址差很多:堆疊那個是 0x7fff…,堆積那個是 0x6146…——它們住在記憶體的兩端。另外 new[] 一定要配 delete[],配錯是未定義行為(g++ 加 -Wall 通常會警告,但別依賴它)。

選一個版本,看堆積上的東西怎麼長、怎麼消。
速度 堆積上還沒還的:0
程式碼 CODE
void build() { int *first = new int(31); int *second = new int(17); delete second; delete first; }
重點 WHY

函式結束時,指標 firstsecond 本身會自動消失,但它們指向的動態整數不會。每個動態物件都需要由 owner 釋放。

① int *p = new int(7); 堆疊 堆積 p 7 正常使用 p 指著堆積上那塊 7 這時 *p 讀得到 7 ② delete p; 堆疊 堆積 p ? 懸空指標 dangling 記憶體還回去了 p 卻還指著,碰它就當掉 ③ p = nullptr; 堆疊 堆積 nullptr 已還給 系統 安全的收尾 箭頭斷乾淨

圖 2:delete 還的是堆積上那塊記憶體,不是指標 p —— p 還留著舊門牌,就成了懸空指標。多寫一行 p = nullptr;,箭頭才真的斷乾淨。

PART 08 · 三類錯誤

指標常見的三類錯誤

共同點:它們都可能通過編譯。這三個名字值得記起來,因為除錯時可以依症狀逐項檢查。

錯誤常見寫法症狀
記憶體洩漏
memory leak
new 了沒 delete程式跑越久占用越多記憶體,最後可能因資源不足而終止。短程式通常看不出來
懸空指標
dangling pointer
delete 之後還在用讀到垃圾值,或當掉。最難 debug,因為有時候「剛好」還是對的
二次釋放
double free
同一塊 delete 兩次屬於未定義行為;可能崩潰,也可能出現難以追蹤的錯誤
🎯 一個習慣可以擋掉兩種

delete p; 之後立刻p = nullptr;。這樣懸空指標會變成空指標(當掉得很明確、很好找),而 delete 一個 nullptr 在 C++ 是合法的空操作,二次釋放也就不會發生。

✅ 現代 C++ 對照

現代 C++ 常用 std::unique_ptr 表達獨佔所有權,讓管理物件離開範圍時自動清理資源。本頁先用手動 newdelete 展示配置、所有權與釋放如何配對;理解原理後,再用管理型別降低遺漏清理的風險。

隨堂 1 · 誰該負責還

一個函式裡 new 出一個裝置物件、把指標回傳給呼叫端。這塊記憶體該誰 delete

(A) 函式自己,在 return 之前
(B) 呼叫端,用完之後
(C) 不用管,程式會自己回收
(D) 編譯器在下一次配置時自動釋放
PART 09 · 收尾

從指標到 Device:以可選設定物件整合本頁語法

Device 保存一個可能尚未建立的 Settings 位址;這個具名關聯同時用到指標成員、nullptr、箭號與所有權約定。以下用最小的 structclass 語法表示兩種物件:struct 的成員預設公開;classpublic: 公開建構與存取操作、以 private: 隱藏內部狀態。建構式與類別同名,建立物件時會執行;P8 再完整介紹類別語法。

getSettings() const 括號後的 const 是最小的唯讀承諾:呼叫這個查詢操作時,不會經由它改動 Device 的一般成員;P8 會完整解釋成員函式的 const

struct Settings { int level; }; class Device { private: int id; Settings *settings; // 可選的具名關聯 public: Device(int initialId) { id = initialId; settings = nullptr; // 一開始尚未設定 } Settings *getSettings() const { return settings; } void setSettings(Settings *value) { settings = value; } };
💡 三個「為什麼」,現在都答得出來了

為什麼 settings 是指標?因為設定物件可能尚不存在,也可能在執行期間替換。
為什麼建構式設成 nullptr這建立了明確且可檢查的「尚未設定」狀態。
誰負責 delete這個簡化的 Device 沒有自動釋放設定物件;範例約定呼叫端管理生命週期,替換時先釋放舊設定,最後也要釋放仍保存的設定。P8、P9 再介紹如何把所有權責任封裝進類別。

按「建立設定」加入幾個候選值,再逐一檢查指標是否有效。
速度
保存具名關聯 CODE
void replaceSettings(Device *device, int level) { Settings *fresh = new Settings{level}; delete device->getSettings(); device->setSettings(fresh); }

先建立新物件,確認成功後才釋放舊設定並保存新位址。若直接覆寫唯一 owner,舊配置就會洩漏。

隨堂 2 · 覆寫 owner 會怎樣

若直接執行 device->setSettings(new Settings{8});,卻沒有先釋放原本由 device 獨佔的設定,會發生什麼?

(A) 編譯錯誤
(B) 原設定物件會洩漏
(C) 舊設定會自動 delete
(D) settings 同時指向兩個物件
🔗 指標成員表達具名關聯

Device::settings 表示裝置與設定物件之間的可選關聯。因為 settings 是 private,外部應先確認 device != nullptr,再呼叫 device->getSettings();回傳值也不是 nullptr 時,才可用 device->getSettings()->level 取得設定值。

EX · 隨堂練習

再一題確認觀念

EXERCISE · 這段程式漏了什麼

int *a = new int[10]; 用完之後寫 delete a;。問題在哪?

(A) 沒問題
(B) new[] 要配 delete[],寫成 delete 是未定義行為
(C) 配置的語法就錯了
(D) 在 delete 前把 a 設成 nullptr 即可
REFERENCE · 速查表

P4 速查表

語法

寫法意思
int *p;宣告一個指向 int 的指標
&xx 的位址
*p解參考:p 指向的那個東西
p->field等同 (*p).field,指向物件時用這個
a[i]等同 *(a + i)
new T(v)在堆積配一個 T,回傳指標
new T[n]在堆積配 n 個 T
delete p;還一個
delete[] p;還一整排
sizeof(x)x 佔幾個 byte(編譯期就決定)

四條記得住的規則

① 每寫一個 new,當場想清楚它的 delete 在哪。
new[]delete[],中括號要成對。
delete 之後立刻把指標設成 nullptr
④ 解參考之前先確認不是 nullptr

延伸閱讀

資源看什麼
C++ Tutor把本頁的每一段程式貼進去,一步一步看堆疊與堆積上的方框和箭頭。學指標最有效的工具。
cppreference · new expressionnew 的完整語意,包含配置失敗時會發生什麼。
cppreference · Pointer指標、空指標與指標運算的語言參考。
P8 · 類別與物件把指標成員、封裝與物件生命週期放進類別介面。
QUIZ · 自我檢測

自我檢測:陣列、指標與動態記憶體 隨堂自測 · 7 題

每個選項都有解說:選錯也點開看看為什麼錯。全對之後再往下翻詞彙卡。

Q1.以下程式印出什麼?
int x = 42;
int *p = &x;
*p = 99;
cout << x << " " << *p;
Q2.以下程式印出什麼?
int a[5] = {10, 20, 30, 40, 50};
cout << a[2] << " " << *(a + 2);
Q3.假設本題執行環境 sizeof(int) 為 4、sizeof(int*) 為 8,main 與函式內分別印出什麼?
#include <iostream>
using namespace std;
void f(int arr[]) {
    cout << sizeof(arr) << endl;
}

int main() {
    int a[5];
    cout << sizeof(a) << endl;
    f(a);
}
Q4.假設以下動態配置均成功,這段函式有什麼問題?
struct Settings { int level; };
struct Device { int id; Settings* settings = nullptr; };
void build() {
    Device* device = new Device{31};
    device->settings = new Settings{17};
}
Q5.int *a = new int[10]; 用完之後寫 delete a; 有什麼問題?
Q6.為什麼 delete 之後常把唯一 owner 指標設成 nullptr?
Q7.假設 active 原先獨佔一個動態 Device,且新配置成功,這段替換函式有什麼問題?
struct Device { int id; };
void replace(Device*& active, int id) {
    active = new Device{id};
}
CARDS · 關鍵詞彙卡

關鍵詞彙卡:點卡片翻面

先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。