指標和參考是 C++ 最重要也最容易混淆的概念之一。理解它們是掌握動態記憶體管理、多型、資料結構的基礎。
建議授課時數:2 小時(含 10 分鐘休息)
學習目標
- 理解指標的概念:位址、宣告、解參考
- 掌握
&(取址)和*(解參考)運算子 - 理解
nullptr與空指標安全 - 認識
const指標的各種組合 - 學會指標算術與陣列的關係
- 理解參考(reference)的語意與用法
- 區分指標與參考的適用場景
- 掌握函式指標的宣告、使用與 callback 模式
- 認識
std::function作為現代替代方案 - 理解 C 風格字串、二維陣列與指標的關係
- 掌握 qsort / std::sort 的 callback 用法與指向成員的指標
- 認識 RAII 與智慧指標(
unique_ptr/shared_ptr/weak_ptr)作為現代記憶體管理基礎
課程時間表(120 分鐘)
| 時段 | 主題 | 內容 | 範例 |
|---|---|---|---|
| 0:00–0:25 | 指標基礎 | 位址概念、宣告、解參考、nullptr | pointers.cpp §1–3 |
| 0:25–0:40 | const 指標 & 函式參數 | 四種 const 組合、swap 範例 | pointers.cpp §4–5 |
| 0:40–0:55 | 指標算術 | 遞增、指標差、陣列走訪、雙指標法 | pointer_arithmetic.cpp |
| 0:55–1:05 | 休息 | ||
| 1:05–1:20 | 參考 | 別名、const 參考、回傳參考 | references.cpp |
| 1:20–1:35 | 函式指標 | 宣告、callback、std::function | function_pointers.cpp |
| 1:35–1:50 | 進階指標 | C 字串、二維陣列、qsort/sort、指向成員 | advanced_pointers.cpp |
| 1:50–2:05 | 智慧指標 | RAII、unique/shared/weak_ptr、擁有權 | smart_pointers.cpp |
| 2:05–2:15 | 總結 & 練習 | 常見陷阱、比較表、課堂練習 | — |
建議每 15–20 分鐘穿插一次現場 demo 或小測驗,維持學員專注力。 若時間不足,「進階指標」與「智慧指標」可視班級程度選擇性講授或作為延伸內容。
一、什麼是指標?
1.1 核心概念
指標是一個儲存記憶體位址的變數。一般變數存「值」,指標存「某個值所在的位置」。
int x = 42;
int* ptr = &x; // ptr 儲存 x 的位址
std::cout << x; // 42(值)
std::cout << &x; // 0x7fff...(位址)
std::cout << ptr; // 0x7fff...(與 &x 相同)
std::cout << *ptr; // 42(解參考:取得該位址的值)
std::cout << &ptr; // ptr 自身所在的位址(不同於 ptr 儲存的值)
1.2 記憶體示意圖
變數名稱 位址 值
x [0x1000] 42
ptr [0x1008] 0x1000 ←─ 指向 x 的位址
1.3 為什麼需要指標?
| 用途 | 說明 |
|---|---|
| 間接修改 | 函式透過指標修改呼叫者的變數 |
| 動態配置 | new / delete 在執行期分配記憶體 |
| 資料結構 | 鏈結串列、樹等節點以指標串接 |
| 效能 | 傳遞大型物件的位址,避免複製 |
| 多型 | 透過基底類別指標操作衍生類別物件 |
二、宣告指標
int* intPtr; // 指向 int 的指標
double* dblPtr; // 指向 double 的指標
char* charPtr; // 指向 char 的指標
int* p1, p2; // ⚠ 只有 p1 是指標,p2 是 int!
int *p3, *p4; // ✓ p3 和 p4 都是指標
指標的大小
在同一平台上,所有型別的指標大小相同(64-bit 系統通常 8 bytes):
sizeof(int*) == sizeof(double*) == sizeof(char*) == sizeof(void*)
// 通常都等於 8(64-bit)或 4(32-bit)
指標的大小取決於位址匯流排寬度,而非指向的型別。
三、nullptr vs NULL vs 0
int* ptr = nullptr; // 空指標(C++11 推薦)
if (ptr != nullptr) {
std::cout << *ptr; // 只在非空時解參考
}
三者的歷史與差異
| 寫法 | 本質 | 問題 |
|---|---|---|
0 |
整數常數 0 | 既是數字又能當空指標,語意模糊 |
NULL |
巨集,通常定義為 0 或 0L |
本質仍是整數,與 0 同樣有歧義 |
nullptr |
關鍵字,型別為 std::nullptr_t |
無歧義,C++11 起的唯一正解 |
歷史上 C 用 NULL(多半 #define NULL 0)表示空指標。但 NULL 骨子裡是整數 0,在 C++ 的多載與模板中會造成錯誤的函式選擇:
void f(int x) { std::cout << "f(int)\n"; }
void f(int* p) { std::cout << "f(int*)\n"; }
f(0); // 呼叫 f(int) —— 0 是整數
f(NULL); // 多半也呼叫 f(int)!因為 NULL 其實是 0(不是你要的指標版本)
f(nullptr); // ✓ 明確呼叫 f(int*),因為 nullptr 只能是指標
C++ Core Guidelines ES.47:用
nullptr而非0或NULL。nullptr型別明確、可讀性高,且不會被誤當成整數。
四、const 與指標
int x = 10;
int y = 20;
// 1. 指向 const 的指標:不能透過指標修改值
const int* p1 = &x;
// *p1 = 99; ← 錯誤!
p1 = &y; // ✓ 可以改指向
// 2. const 指標:不能改指向
int* const p2 = &x;
*p2 = 99; // ✓ 可以改值
// p2 = &y; ← 錯誤!
// 3. const 指標指向 const:都不能改
const int* const p3 = &x;
// *p3 = 99; ← 錯誤!
// p3 = &y; ← 錯誤!
口訣:const 在 * 左邊 → 值不能改;const 在 * 右邊 → 指向不能改。
四種組合速查表
| 宣告 | 改值 | 改指向 | 常見用途 |
|---|---|---|---|
int* p |
✓ | ✓ | 一般用途 |
const int* p |
✗ | ✓ | 唯讀存取(函式參數) |
int* const p |
✓ | ✗ | 固定指向某變數 |
const int* const p |
✗ | ✗ | 完全唯讀 |
五、指標作為函式參數
延續 Ch04 的參數傳遞,指標是第三種方式:
// 傳值 — 複製,不影響原始值
void byValue(int x) { x = 0; }
// 傳參考 — 別名,直接修改
void byRef(int& x) { x = 0; }
// 傳指標 — 傳位址,需解參考
void byPtr(int* x) { *x = 0; }
int main() {
int n = 42;
byValue(n); // n 仍為 42
byRef(n); // n 變為 0
byPtr(&n); // n 變為 0(呼叫時需加 &)
}
swap 範例
void swapByPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 呼叫:swapByPtr(&m, &n);
課堂討論:指標參數 vs 參考參數,何時選哪個?
六、指標算術
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // 陣列名就是第一個元素的位址
std::cout << *p; // 10
std::cout << *(p + 1); // 20
std::cout << *(p + 2); // 30
p++; // p 移動 sizeof(int) 個位元組
std::cout << *p; // 20
陣列與指標的關係
arr[i] 等同於 *(arr + i)
&arr[i] 等同於 arr + i
指標算術規則
| 運算 | 說明 | 範例 |
|---|---|---|
p + n |
向前移 n 個元素 | *(arr + 2) → arr[2] |
p - n |
向後移 n 個元素 | |
p++ / p-- |
移動一個元素 | 走訪陣列 |
p1 - p2 |
兩指標間的元素個數差 | end - begin |
p1 < p2 |
比較位址先後 | 同一陣列內有效 |
重要限制
- 指標算術只在連續記憶體(陣列)中有意義
- 不可對不相關的指標做減法或比較
- 指向「最後元素 + 1」可用作迴圈邊界,但不可解參考
- 越界存取是未定義行為(UB)
雙指標法反轉陣列
void reverseArray(int* arr, int size) {
int* left = arr;
int* right = arr + size - 1;
while (left < right) {
int temp = *left;
*left = *right;
*right = temp;
left++;
right--;
}
}
七、進階指標概念
7.1 指標的指標(Multi-level Pointer)
int x = 42;
int* p = &x; // 指向 int
int** pp = &p; // 指向 int*(二級指標)
std::cout << x; // 42
std::cout << *p; // 42
std::cout << **pp; // 42(解參考兩次)
常見用途:二維陣列、函式修改指標本身、動態配置指標陣列。
7.2 void* 通用指標
int i = 42;
double d = 3.14;
void* vp = &i; // void* 可指向任意型別
// std::cout << *vp; ← 錯誤!不能直接解參考
int* ip = static_cast<int*>(vp); // 需轉型後才能使用
std::cout << *ip; // 42
void* 常用於 C 函式庫(如 malloc)或需要型別無關的通用介面。
7.3 指向物件的指標(-> 運算子)
struct Point { int x, y; };
Point pt = {10, 20};
Point* ptr = &pt;
// 兩種等價寫法
(*ptr).x = 100; // 先解參考再存取成員
ptr->x = 100; // 箭頭運算子(更常用)
ptr->y = 200;
7.4 動態記憶體(預告 Ch16)
int* dyn = new int(42); // 在 heap 分配
std::cout << *dyn; // 42
delete dyn; // 釋放
dyn = nullptr; // 避免懸垂指標
現代 C++ 建議使用
std::unique_ptr/std::shared_ptr管理動態記憶體。
八、參考(References)
int x = 42;
int& ref = x; // ref 是 x 的別名
ref = 100; // 修改 ref 等於修改 x
std::cout << x; // 100
特性
- 必須在宣告時初始化
- 初始化後不能重新綁定到其他變數
&ref與&x相同(同一記憶體位置)- 不存在「空參考」
常見誤解:ref = b 不是重新綁定
int a = 10, b = 20;
int& ref = a;
ref = b; // 這是把 b 的值賦給 a,不是讓 ref 改綁到 b!
// 結果:a = 20, b = 20
const 參考
const int& cref = x; // 不能透過 cref 修改 x
// cref = 200; ← 錯誤!
const int& temp = 42; // ✓ const 參考可以綁定到暫時值(rvalue)
const 參考作為函式參數是 C++ 的最佳實踐:
void print(const std::string& s); // 避免複製,且不修改原始值
回傳參考
int& getElement(std::vector<int>& vec, size_t i) {
return vec.at(i);
}
// 使用:getElement(vec, 2) = 999; // 直接修改 vec[2]
⚠ 危險:不可回傳區域變數的參考!
int& bad() {
int local = 42;
return local; // 懸垂參考!local 已銷毀
}
九、指標 vs 參考比較
| 特性 | 指標(int*) |
參考(int&) |
|---|---|---|
| 可為空值 | 是(nullptr) |
否 |
| 可重新指向 | 是 | 否 |
| 語法 | 需 * 和 & |
直接使用 |
| 初始化 | 可延後 | 必須立即 |
| 多級 | 支援(int**) |
不支援 |
| 算術 | 支援(p++) |
不支援 |
| 用途 | 動態記憶體、可選參數 | 函式參數、運算子多載 |
何時使用哪個?
| 情境 | 建議 |
|---|---|
| 函式參數,確定不為空 | 參考 const T& |
| 函式參數,可能為空 | 指標 const T* |
| 需要重新指向 | 指標 |
| 動態記憶體 | 指標(或智慧指標) |
| 陣列 / 緩衝區操作 | 指標 |
| 回傳容器元素 | 參考 |
| 與 C API 互操作 | 指標 |
十、函式指標(Function Pointers)
函式在記憶體中也有位址。函式指標儲存函式的位址,讓程式可以間接呼叫函式,或將函式作為參數傳遞(callback)。
10.1 宣告語法
// 回傳型別 (*指標名)(參數型別列表)
int (*funcPtr)(int, int);
funcPtr = add; // 函式名自動轉為位址
int result = funcPtr(3, 5); // 間接呼叫
// 等價於 (*funcPtr)(3, 5)
解析技巧:
(*funcPtr)被括號包住,表示 funcPtr 是一個指標;後面的(int, int)是參數列表。
10.2 typedef 簡化
typedef int (*BinaryOp)(int, int);
BinaryOp op = add;
std::cout << op(10, 20); // 30
C++11 起也可用 type alias:
using BinaryOp = int (*)(int, int);
10.3 函式指標陣列
int (*operations[])(int, int) = {add, subtract, multiply, divide};
int x = 20, y = 4;
for (int i = 0; i < 4; i++) {
std::cout << operations[i](x, y) << "\n";
}
這是策略模式的 C 風格實現,在嵌入式系統和圖形 API 中很常見。
10.4 函式指標作為 Callback
// 高階函式:接收一個函式指標
int apply(int a, int b, int (*op)(int, int)) {
return op(a, b);
}
// 使用
apply(10, 3, add); // 13
apply(10, 3, multiply); // 30
典型應用場景:
| 場景 | 說明 |
|---|---|
| qsort | C 標準庫排序,傳入比較函式 |
| 事件處理 | GUI 按鈕點擊 callback |
| 過濾 / 映射 | 對集合每個元素執行指定操作 |
| 插件系統 | 動態載入並呼叫函式 |
10.5 回傳函式指標
int (*selectOp(char op))(int, int) {
switch (op) {
case '+': return add;
case '-': return subtract;
default: return add;
}
}
auto func = selectOp('+');
func(5, 3); // 8
10.6 函式指標 vs 函式參考
void callByPtr(int (*f)(int, int)); // 函式指標參數
void callByRef(int (&f)(int, int)); // 函式參考參數(較少見)
函式參考不能為 null,且語法較簡潔,但函式指標更通用。
十一、現代替代方案:std::function 與 Lambda
11.1 std::function
#include <functional>
int calculate(int* arr, int size,
const std::function<int(int, int)>& op) {
int result = arr[0];
for (int i = 1; i < size; i++)
result = op(result, arr[i]);
return result;
}
std::function 是可儲存任意 callable 的泛型包裝器:函式指標、lambda、functor 皆可。
11.2 Lambda 與函式指標
// 無 capture → 可轉為函式指標
int (*fp)(int, int) = [](int a, int b) { return a + b; };
// 有 capture → 必須用 std::function
int factor = 2;
std::function<int(int, int)> fn =
[factor](int a, int b) { return (a + b) * factor; };
11.3 選擇指南
| 機制 | Overhead | 支援 Capture | 適用場景 |
|---|---|---|---|
| 函式指標 | 零 | 否 | C 互操作、效能關鍵 |
std::function |
小(可能 heap 配置) | 是 | 通用 callback API |
| template + auto | 零(編譯期) | 是 | 內部高效能程式碼 |
| lambda(無 capture) | 零 | 否 | 簡單 inline 邏輯 |
初學階段先掌握函式指標;進階後以
std::function和 lambda 為主。
十二、C 風格字串與字元指標
C++ 同時支援 std::string 與「C 風格字串」。C 字串本質是以 '\0'(空字元)結尾的 char 陣列,字串字面量在表達式中會退化為 const char*。
const char* msg = "Hello"; // 指向唯讀字串字面量
// 'H' 'e' 'l' 'l' 'o' '\0' ← 編譯器自動加上結尾的 '\0'
12.1 用指標走訪字串
走訪直到遇到結尾的 '\0':
for (const char* p = msg; *p != '\0'; ++p) {
std::cout << *p << " ";
}
12.2 雙指標反轉字串
char buf[] = "ABCDEF"; // 可修改的字元陣列
char* left = buf;
char* right = buf + std::strlen(buf) - 1;
while (left < right) {
std::swap(*left, *right); // 或手動交換
++left;
--right;
}
// buf 變成 "FEDCBA"
⚠ 注意:
const char*指向的字串字面量唯讀,嘗試修改是未定義行為。要修改字串請使用char[]陣列。 現代 C++ 一般優先使用std::string;char*主要用於 C API 互操作與底層處理。
十三、二維陣列與指標
二維陣列在記憶體中是連續排列(row-major,列優先)。
int matrix[2][3] = {{1, 2, 3},
{4, 5, 6}};
matrix[i][j] 等同於 *(*(matrix + i) + j)
指向「陣列」的指標
int (*rowPtr)[3] = matrix; // 指向「含 3 個 int 的陣列」的指標
rowPtr[i][j]; // 等同 matrix[i][j]
辨析:
int (*p)[3](指向陣列的指標)與int* p[3](指標陣列)是完全不同的東西! -int (*p)[3]→ 一個指標,指向含 3 個 int 的陣列 -int* p[3]→ 一個陣列,含 3 個int*指標
十四、callback 實戰:qsort、std::sort 與指向成員的指標
14.1 qsort(C 標準庫排序)
qsort 是函式指標 callback 最經典的應用。它型別無關(透過 void*),由你提供比較函式:
#include <cstdlib>
int ascendingCmp(const void* a, const void* b) {
int x = *static_cast<const int*>(a);
int y = *static_cast<const int*>(b);
return (x > y) - (x < y); // 回傳負/零/正
}
int data[] = {5, 2, 8, 1, 9, 3};
std::qsort(data, 6, sizeof(int), ascendingCmp);
14.2 std::sort(型別安全的現代替代)
C++ 的 std::sort 接受 lambda 或 functor,型別安全且通常更快(可 inline):
#include <algorithm>
std::sort(data, data + 6, [](int a, int b) { return a < b; }); // 升序
std::sort(data, data + 6, [](int a, int b) { return a > b; }); // 降序
| 比較 | qsort(C) |
std::sort(C++) |
|---|---|---|
| 比較器 | 函式指標 + void* |
任意 callable(lambda / functor) |
| 型別安全 | 否(需手動轉型) | 是 |
| 效能 | 無法 inline | 可 inline,通常更快 |
| 適用 | C 互操作 | C++ 首選 |
14.3 指向成員的指標(Pointer to Member)
成員函式有隱藏的 this,因此不能用一般函式指標儲存,需用指向成員的指標:
struct Calculator {
int value = 0;
int add(int x) { return value + x; }
int multiply(int x) { return value * x; }
};
// 宣告:回傳型別 (類別::*名稱)(參數)
int (Calculator::*opPtr)(int) = &Calculator::add;
Calculator calc; calc.value = 10;
(calc.*opPtr)(5); // 透過物件呼叫 → 用 .*
Calculator* pc = &calc;
(pc->*opPtr)(5); // 透過指標呼叫 → 用 ->*
std::mem_fn 可把成員函式包成一般可呼叫物件:
#include <functional>
auto fn = std::mem_fn(&Calculator::add);
fn(calc, 7); // 等同 calc.add(7)
指向成員的指標常用於選單、狀態機、事件分派表。
十五、RAII 與智慧指標(現代記憶體管理)
裸指標(new / delete)容易出錯:忘記 delete(記憶體洩漏)、delete 兩次(double free)、提前 return 或丟出例外導致 delete 未執行。智慧指標用 RAII 自動解決這些問題。
RAII(Resource Acquisition Is Initialization):把資源(記憶體、檔案、鎖)的生命週期綁定到物件的生命週期。物件離開作用域時,解構子自動釋放資源。
15.1 unique_ptr — 獨佔擁有權(首選)
#include <memory>
auto p = std::make_unique<Resource>("A"); // 優先用 make_unique,而非 new
p->use();
// 不可複製,但可移動(轉移擁有權)
auto q = std::move(p); // 此後 p == nullptr
// 也能管理陣列
auto arr = std::make_unique<int[]>(5);
- 零額外開銷,行為等同裸指標但安全
- 離開作用域時自動 delete,不需手動釋放
15.2 shared_ptr — 共享擁有權(參考計數)
auto a = std::make_shared<Resource>("B"); // use_count = 1
{
auto b = a; // 複製 → use_count = 2
} // b 離開作用域 → use_count = 1
// 當最後一個 shared_ptr 銷毀時才釋放物件
- 透過參考計數追蹤擁有者數量
- 計數操作是 atomic,有少量開銷 → 熱路徑避免反覆複製
15.3 weak_ptr — 非擁有觀察者(打破循環)
兩個 shared_ptr 互相持有會形成循環參考,計數永遠歸不了零 → 記憶體洩漏。用 weak_ptr 觀察而不增加計數:
struct Node {
std::shared_ptr<Node> next; // 擁有
std::weak_ptr<Node> prev; // 觀察(不擁有,打破循環)
};
if (auto p = node->prev.lock()) { // lock() 安全取得,物件還在才成功
p->use();
}
15.4 用參數型別表達擁有權語意(Core Guidelines)
| 你想表達 | 函式參數寫法 |
|---|---|
| 函式接管擁有權 | 以值傳 std::unique_ptr<T> |
| 函式共享擁有權 | 以值傳 std::shared_ptr<T> |
| 函式只是使用(不擁有) | 傳 T* 或 const T& |
| 函式會重新指派智慧指標 | 傳 unique_ptr<T>& |
15.5 選擇指南
| 需求 | 建議 |
|---|---|
| 預設、獨佔擁有 | unique_ptr(R.21:優先用 unique_ptr) |
| 真的需要多方共享 | shared_ptr |
| 觀察但不擁有 / 打破循環 | weak_ptr |
| 建立物件 | make_unique / make_shared(R.22/R.23) |
智慧指標屬於 Ch16 的核心主題,本章先建立「擁有權」的觀念,理解為什麼現代 C++ 幾乎不再裸用
new/delete。
十五點五、最佳實踐(Best Practices)
對應 C++ Core Guidelines,把以下原則內化,能避免絕大多數指標相關的 bug:
- F.60 / F.7:能用參考就別用指標。 當參數一定存在、不需重新指向時用
T&;只有「可能不存在(可選)」或「需要重新指向」才用T*。 - ES.47:用
nullptr,不要用0或NULL。 型別明確、避免多載歧義。 - ES.65:先檢查再解參考。 解參考前確認指標非空(
if (p) ...),尤其是來自外部的指標。 - ES.42:保持指標用法簡單直接。 指標算術只在陣列範圍內進行;越界即 UB。
- R.10 / R.11:避免裸
new/delete。 動態記憶體交給智慧指標(make_unique/make_shared)以 RAII 自動管理。 - R.20 / R.21:預設用
unique_ptr,真的要共享才用shared_ptr。 表達擁有權;非擁有者用T*或T&。 - I.11 / F.26:別用裸指標傳遞擁有權。 函式參數的裸指標代表「只是借用、不負責釋放」。
- Con 系列:const 正確性。 不修改指向值就用
const T*;用「const 在*左邊管值、右邊管指向」的口訣。 - 回傳值別回傳區域變數的指標 / 參考(懸垂);只回傳生命週期更長的物件。
- 參考不能重綁、不能為空——優先用它表達「保證有效的別名」,讓介面更安全。
一句話總結現代 C++ 的指標哲學:「擁有權交給智慧指標,借用就用參考或
const T*,裸new/delete幾乎絕跡。」
十六、常見陷阱
| # | 陷阱 | 說明 | 預防 |
|---|---|---|---|
| 1 | 野指標 | 未初始化的指標指向隨機位址 | 初始化為 nullptr 或有效位址 |
| 2 | 解參考空指標 | 程式 crash | 先檢查 ptr != nullptr |
| 3 | 懸垂指標 | 指向已釋放或已銷毀的記憶體 | delete 後設 nullptr |
| 4 | 懸垂參考 | 回傳區域變數的參考 | 只回傳存活期更長的物件 |
| 5 | const 位置搞混 | const int* vs int* const |
記住口訣 |
| 6 | 指標宣告陷阱 | int* a, b; 只有 a 是指標 |
一行一個宣告 |
| 7 | 指標越界 | 存取陣列外的記憶體 | 確保索引在 [0, size) |
| 8 | 混淆 ref = b | 以為是重新綁定 | ref = b 是賦值,不是改綁 |
| 9 | 函式指標型別不匹配 | 參數或回傳型別不同 | 嚴格匹配簽名 |
| 10 | 記憶體洩漏 | new 後忘記 delete |
改用智慧指標(RAII) |
| 11 | double free | 同一指標 delete 兩次 |
delete 後設 nullptr;用智慧指標 |
| 12 | 循環參考 | 兩個 shared_ptr 互相持有 |
其中一方改用 weak_ptr |
| 13 | 修改字串字面量 | 透過 const char* 改 "..." |
要改就用 char[] 陣列 |
| 14 | 混淆 int(*p)[3] / int* p[3] |
指向陣列 vs 指標陣列 | 看 * 是否被括號包住 |
懸垂指標範例
int* dangling() {
int local = 42;
return &local; // local 在函式結束後銷毀 → 懸垂指標!
}
十七、重點整理
| 概念 | 要點 |
|---|---|
& 運算子 |
取得變數的記憶體位址 |
* 運算子 |
解參考,取得指標指向的值 |
-> 運算子 |
透過指標存取結構 / 類別成員 |
nullptr |
空指標,永遠用它取代 NULL |
const 指標 |
分清「指向 const」和「const 指標」 |
| 指標算術 | 每次移動 sizeof(型別) 個位元組 |
| 參考 | 別名、必須初始化、不能為空 |
| 函式指標 | int (*fp)(int, int) = func; |
| callback | 將函式指標傳入另一函式,延遲決定行為 |
std::function |
現代 callback 包裝,支援 lambda |
| C 字串 | 以 '\0' 結尾的 char 陣列 |
| 二維陣列 | 連續記憶體,a[i][j] = *(*(a+i)+j) |
| 指向成員 | (obj.*p)(...)、(ptr->*p)(...) |
| RAII | 資源生命週期綁定物件生命週期 |
unique_ptr |
獨佔擁有、零開銷、首選 |
shared_ptr |
共享擁有、參考計數 |
weak_ptr |
非擁有觀察、打破循環參考 |
十八、課堂活動
活動 1:記憶體位址觀察(10 分鐘)
執行 pointers.cpp,觀察:
- 同一變數的值與位址
- 指標的值(指向的位址)vs 指標自身的位址
- 修改 *ptr 如何影響原始變數
活動 2:const 指標實驗(10 分鐘)
宣告四種 const 指標組合,逐一嘗試修改值和指向,記錄編譯器錯誤訊息。
活動 3:雙指標反轉(15 分鐘)
用 pointer_arithmetic.cpp 中的 reverseArray,手動 trace left 和 right 的移動過程。
活動 4:計算機 callback(15 分鐘)
修改 function_pointers.cpp,加入一個互動式計算機:
- 提示使用者輸入運算符(+、-、*、/)
- 用 selectOperation 選擇對應函式
- 印出結果
活動 5:指標 vs 參考辯論(5 分鐘)
分組討論:「設計一個 find() 函式,找不到時回傳什麼?」
- 指標方案:回傳 nullptr
- 參考方案:無法表示「找不到」
- 引出後續 std::optional 等現代方案
活動 6:qsort vs std::sort(10 分鐘)
執行 advanced_pointers.cpp,比較 qsort(函式指標 + void*)與 std::sort(lambda)的寫法差異,討論型別安全與效能。
活動 7:觀察智慧指標的生命週期(15 分鐘)
執行 smart_pointers.cpp,從建構 / 解構訊息觀察:
- unique_ptr 移動後原指標變為空
- shared_ptr 的 use_count 隨複製 / 離開作用域增減
- weak_ptr 如何讓 Node 正確解構(將 prev 改回 shared_ptr 觀察洩漏)
十九、練習題
基礎
- 交換函式:分別用指標和參考寫
swap函式,比較兩者的呼叫語法。 - const 練習:宣告四種
const指標組合,嘗試修改並觀察編譯器錯誤訊息。 - nullptr 安全:寫函式接收
int*,若為nullptr則印出警告並安全返回。
進階
- 找最大值:寫函式接收整數陣列(用指標 + 大小),回傳最大值的指標。
- 反轉陣列:用指標算術實作陣列反轉(雙指標法)。
- 參考回傳:寫函式回傳
vector中最大元素的參考,並透過參考修改它。 - 字串反轉:用兩個
char*指標反轉 C 風格字串(不需#include <algorithm>)。
函式指標
- 排序 callback:寫
sortDescending(int* arr, int size, bool (*compare)(int, int)),用函式指標控制升序 / 降序。 - 簡易選單:用函式指標陣列實作文字選單(1=加、2=減、3=乘、4=除),使用者選擇後執行對應運算。
- std::function 改寫:將第 8 題改用
std::function和 lambda 實作,比較程式碼差異。
進階指標
- 回文判斷:用兩個
char*指標判斷 C 風格字串是否為回文。 - 二維走訪:用
int (*p)[N]row 指標印出二維陣列的轉置(transpose)。 - qsort 結構排序:定義
struct Student { char name[20]; int score; },用qsort依score降序排序。 - 成員函式分派表:用指向成員函式的指標陣列實作一個小型計算機選單。
智慧指標
- unique_ptr 工廠:寫
std::unique_ptr<Shape> createShape(...)工廠函式,回傳不同形狀並自動管理記憶體。 - shared 計數觀察:用
make_shared建立物件,在不同作用域複製,印出每一步的use_count。 - 打破循環:寫一個雙向鏈結節點,先用兩個
shared_ptr觀察記憶體洩漏,再把prev改成weak_ptr修正。
二十、課後測驗(口頭 / 選擇)
int* p和int& r的主要差異是什麼?const int* p和int* const p有何不同?- 為什麼
arr[i]等價於*(arr + i)? - 以下哪些會造成懸垂指標?
- (a) 回傳區域變數的指標
- (b)
delete後繼續使用指標 - (c) 指標指向已 pop 的 vector 元素 - 函式指標
int (*fp)(int, int)的*代表什麼? - 為什麼 capturing lambda 不能轉換為函式指標?
- C 風格字串如何判斷結尾?修改字串字面量會發生什麼事?
int (*p)[3]和int* p[3]有何不同?- 為什麼成員函式不能用一般函式指標儲存?要用什麼語法呼叫?
unique_ptr與shared_ptr的擁有權語意差異是什麼?何時該用weak_ptr?- 什麼是 RAII?智慧指標如何利用它避免記憶體洩漏?
- 為什麼應優先用
make_unique/make_shared而非new?
對應範例程式
| 檔案 | 說明 | 建議時段 |
|---|---|---|
| pointers.cpp | 指標基礎:宣告、解參考、nullptr、const 指標 | 第 1–2 段 |
| pointer_arithmetic.cpp | 指標算術:遞增、陣列走訪、指標差、雙指標法 | 第 3 段 |
| references.cpp | 參考:宣告、別名、函式參數、const 參考 | 第 4 段 |
| function_pointers.cpp | 函式指標:callback、函式指標陣列、std::function | 第 5 段 |
| advanced_pointers.cpp | 進階:C 字串、二維陣列、qsort/sort、指向成員 | 第 6 段 |
| smart_pointers.cpp | 智慧指標:RAII、unique/shared/weak_ptr、擁有權 | 第 7 段 |
編譯與執行
g++ -std=c++17 -Wall -o pointers pointers.cpp && ./pointers
g++ -std=c++17 -Wall -o pointer_arithmetic pointer_arithmetic.cpp && ./pointer_arithmetic
g++ -std=c++17 -Wall -o references references.cpp && ./references
g++ -std=c++17 -Wall -o function_pointers function_pointers.cpp && ./function_pointers
g++ -std=c++17 -Wall -o advanced_pointers advanced_pointers.cpp && ./advanced_pointers
g++ -std=c++17 -Wall -o smart_pointers smart_pointers.cpp && ./smart_pointers
延伸閱讀
- cppreference — Pointers
- cppreference — References
- cppreference — Function pointers
- cppreference — std::function
- cppreference — Memory management library(智慧指標)
- cppreference — std::unique_ptr
- cppreference — std::shared_ptr
- cppreference — std::mem_fn
- cppreference — std::qsort
- C++ Core Guidelines — R: Resource management
- ISO C++ FAQ — Pointers to members
- 下一章預告:Ch07 將結合指標與結構,Ch16 深入動態記憶體管理