① 這頁從語法開始建立陣列、位址、指標與動態記憶體的完整觀念。已經熟的可以直接跳過。
② 每節都附一個「這在資料結構課哪裡會用到」的小方框——那才是你該記住的部分。
③ 讀完做 自我檢測,再翻 關鍵詞彙卡。
先確認能分辨指標與所指物件,再練習配置與釋放。
指標把物件的位置變成可保存、比較與傳遞的值,也讓程式能表達「目前沒有對象」與執行期才建立的物件:
| 一般情境 | new | nullptr | 指標扮演的角色 |
|---|---|---|---|
| 可選的裝置設定 | 可用 | 未設定 | 裝置以指標參照可能不存在的設定物件 |
| 目前選取的文件 | 可用 | 未選取 | 指標可在不同文件物件之間改指 |
| 大型影像緩衝區 | 動態 | 配置失效後清空 | 大小在執行時決定,owner 負責釋放 |
| 函式的可選輸出 | 依介面 | 沒有結果 | 呼叫端先檢查,再透過指標取得物件 |
| 共同要求 | 明確 owner | 先檢查 | 所有位址都必須指向仍存活且型別正確的物件 |
一般變數讓你直接使用值;指標則把值所在的位址也變成可保存、比較與傳遞的資料。這份控制能力伴隨一項責任:使用位址前,必須確認目標仍然存在。
先依序讀懂 &、*、nullptr 與生命週期,再用 Device/Settings 的具名關聯整合這些語法。
#include <iostream>
int main() {
int value = 42;
int *address = &value;
std::cout << "值 = " << *address << '\n';
}
變數不是憑空存在的,它佔一塊記憶體。&x 就是問「x 住在哪」,sizeof(x) 是問「它佔幾格」。
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 會把它當字串開頭。
int *p:宣告 p 是「指向 int 的指標」
&x:取出 x 的位址
*p:順著 p 存的位址走進去
圖 1:x 盒裡裝的是值 25,p 盒裡裝的是「x 的位址」——所以 *p = 50; 改的是箭頭那一頭的 x,不是 p 自己。上面的動畫演逐步,這張圖給你前後對照。
int *p = &x;:三個符號,三件事初學最容易混的是:* 在宣告時和在使用時意思不一樣。
| 寫法 | 出現在哪 | 意思 |
|---|---|---|
| int *p; | 宣告 | p 是一個指標,指向 int |
| p = &x; | 使用 | 把 x 的位址存進 p |
| *p | 使用 | 解參考:p 指向的那個東西(就是 x 本人) |
| *p = 99; | 使用 | 透過 p 改動 x |
| p | 使用 | 指標本身的值,也就是一個位址 |
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 才走得進去 |
| 本套頁面的典型用途 | 函式參數與回傳(P3:T&、const T&) | 可選的 settings、目前選取的物件、new 的回傳值 |
只是想讓函式碰到本人、或省下複製:用參考,呼叫端乾淨、也不必防
nullptr。需要「可能沒有對象」或「會換對象」的語意,例如裝置的
settings 尚未建立,或 selected 要改指另一個物件,就需要使用指標。
下列哪件事是指標做得到、參考做不到的?
nullptr:指向「哪裡都不是」 空目標的標準寫法nullptr 代表「這個指標目前沒有指向任何物件」。它可表示一項可選關聯目前尚未設定。
這裡先用一個固定陣列保存四個指標:int* choices[4] 中,int* 是每格的型別,choices 是名稱,[4] 是格數,大括號依序給初值。範圍 for 每次取出一格的指標,再檢查是否為 nullptr;下一節會完整拆解陣列與索引。
31 17 93
第二個元素是 nullptr,表示該位置沒有整數可讀。程式先檢查再解參考 *current,因此會安全跳過空項目。
nullptr*current 在 current 是 nullptr 時是未定義行為;可能崩潰,也可能呈現其他結果。必須先檢查指標再解參考。
C++11 之後的 nullptr 有專用型別,能避免舊式整數空值在多載中造成歧義;本頁一律使用它。
裝置設定、目前工作或快取結果都可能暫時不存在。原始指標用 nullptr 表達「沒有對象」時,每次解參考前都必須先檢查。
a[i] 其實就是 *(a + i)陣列本身有陣列型別,不是指標變數。它在許多運算式中會轉換成指向首元素的指標;此時 a[i] 等價於 *(a + i)。sizeof(a) 與 &a 等情況不作這種轉換,因此不能一律把陣列名稱當成位址。
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 格」,也就是原地。
二維陣列採 row-major 佈局時,matrix[i][j] 位於第 i * 欄數 + j 個元素。這個公式正是由一維指標算術延伸而來。
指標算術說明低階陣列的位址計算,也能推導二維陣列的 row-major 位置公式;實務上仍應避免讓指標越過陣列合法範圍。
在 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 個測試函式」——迴圈跑四個函式不用寫四段重複的碼。
能這樣塞是因為函式名跟陣列名一樣,會退化成指標(正是本節講的那件事)。
這是函式指標與陣列退化的延伸例子,先能由內往外讀懂宣告即可。
| 堆疊 stack | 堆積 heap | |
|---|---|---|
| 怎麼配置 | 宣告區域變數就有 | 要自己寫 new |
| 怎麼釋放 | 離開範圍自動消失 | 要自己寫 delete |
| 可用空間 | 通常較小,由執行環境決定 | 通常較大,同樣受系統資源限制 |
| 元素數量何時決定 | 區域物件的型別與大小固定;進入作用域時建立 | new T[n] 的 n 可在執行時決定 |
| 本套頁面用在哪 | 迴圈變數、暫存值 | 執行期配置的設定、緩衝區與其他物件 |
函式呼叫太深可能耗盡堆疊;new 建立的物件若失去可用指標而未釋放,則會造成堆積上的記憶體洩漏。
new 與 delete 配置與釋放必須配對堆疊變數位址 = 0x7fff825e572c 堆積變數位址 = 0x6146265982b0 兩者的值都是 42 / 42 arr = 0 1 4 9 16 都還回去了。
注意兩個位址差很多:堆疊那個是 0x7fff…,堆積那個是 0x6146…——它們住在記憶體的兩端。另外 new[] 一定要配 delete[],配錯是未定義行為(g++ 加 -Wall 通常會警告,但別依賴它)。
函式結束時,指標 first 與 second 本身會自動消失,但它們指向的動態整數不會。每個動態物件都需要由 owner 釋放。
圖 2:delete 還的是堆積上那塊記憶體,不是指標 p —— p 還留著舊門牌,就成了懸空指標。多寫一行 p = nullptr;,箭頭才真的斷乾淨。
共同點:它們都可能通過編譯。這三個名字值得記起來,因為除錯時可以依症狀逐項檢查。
| 錯誤 | 常見寫法 | 症狀 |
|---|---|---|
| 記憶體洩漏 memory leak | new 了沒 delete | 程式跑越久占用越多記憶體,最後可能因資源不足而終止。短程式通常看不出來 |
| 懸空指標 dangling pointer | delete 之後還在用 | 讀到垃圾值,或當掉。最難 debug,因為有時候「剛好」還是對的 |
| 二次釋放 double free | 同一塊 delete 兩次 | 屬於未定義行為;可能崩潰,也可能出現難以追蹤的錯誤 |
delete p; 之後立刻寫 p = nullptr;。這樣懸空指標會變成空指標(當掉得很明確、很好找),而 delete 一個 nullptr 在 C++ 是合法的空操作,二次釋放也就不會發生。
現代 C++ 常用 std::unique_ptr 表達獨佔所有權,讓管理物件離開範圍時自動清理資源。本頁先用手動 new/delete 展示配置、所有權與釋放如何配對;理解原理後,再用管理型別降低遺漏清理的風險。
一個函式裡 new 出一個裝置物件、把指標回傳給呼叫端。這塊記憶體該誰 delete?
Device:以可選設定物件整合本頁語法Device 保存一個可能尚未建立的 Settings 位址;這個具名關聯同時用到指標成員、nullptr、箭號與所有權約定。以下用最小的 struct/class 語法表示兩種物件:struct 的成員預設公開;class 以 public: 公開建構與存取操作、以 private: 隱藏內部狀態。建構式與類別同名,建立物件時會執行;P8 再完整介紹類別語法。
getSettings() const 括號後的 const 是最小的唯讀承諾:呼叫這個查詢操作時,不會經由它改動 Device 的一般成員;P8 會完整解釋成員函式的 const。
為什麼 settings 是指標?因為設定物件可能尚不存在,也可能在執行期間替換。
為什麼建構式設成 nullptr?這建立了明確且可檢查的「尚未設定」狀態。
誰負責 delete?這個簡化的 Device 沒有自動釋放設定物件;範例約定呼叫端管理生命週期,替換時先釋放舊設定,最後也要釋放仍保存的設定。P8、P9 再介紹如何把所有權責任封裝進類別。
先建立新物件,確認成功後才釋放舊設定並保存新位址。若直接覆寫唯一 owner,舊配置就會洩漏。
若直接執行 device->setSettings(new Settings{8});,卻沒有先釋放原本由 device 獨佔的設定,會發生什麼?
Device::settings 表示裝置與設定物件之間的可選關聯。因為 settings 是 private,外部應先確認 device != nullptr,再呼叫 device->getSettings();回傳值也不是 nullptr 時,才可用 device->getSettings()->level 取得設定值。
int *a = new int[10]; 用完之後寫 delete a;。問題在哪?
| 寫法 | 意思 |
|---|---|
| int *p; | 宣告一個指向 int 的指標 |
| &x | x 的位址 |
| *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 expression | new 的完整語意,包含配置失敗時會發生什麼。 |
| cppreference · Pointer | 指標、空指標與指標運算的語言參考。 |
| P8 · 類別與物件 | 把指標成員、封裝與物件生命週期放進類別介面。 |
每個選項都有解說:選錯也點開看看為什麼錯。全對之後再往下翻詞彙卡。
int x = 42; int *p = &x; *p = 99; cout << x << " " << *p;
int a[5] = {10, 20, 30, 40, 50};
cout << a[2] << " " << *(a + 2);
#include <iostream>
using namespace std;
void f(int arr[]) {
cout << sizeof(arr) << endl;
}
int main() {
int a[5];
cout << sizeof(a) << endl;
f(a);
}
struct Settings { int level; };
struct Device { int id; Settings* settings = nullptr; };
void build() {
Device* device = new Device{31};
device->settings = new Settings{17};
}
struct Device { int id; };
void replace(Device*& active, int id) {
active = new Device{id};
}
先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。