指針聲明讀法:右左法則(優先級)
int *p; // p 是指針 → 指向 int
int *p[3]; // p 是數組 → 元素是指針 → 指向 int
int (*p)[3]; // p 是指針 → 指向數組[3] → 元素是 int
int **p; // p 是指針 → 指向指針 → 指向 int
int (*p)(int); // p 是指針 → 指向函數(參數int) → 返回int
- 從變量名開始
- 先看右邊
[] ()(高優先級)
- 再看左邊
*
- 最後看基礎類型
指針四要素
| 要素 | 含義 | 例子:int *p = &a; |
| 指針類型 | 指針變量本身的類型 | int * |
| 指向類型 | 解引用後得到的類型 | int |
| 指針值 | 存儲的地址數值 | &a (如 0x7ffc...) |
| 指針自身內存 | 指針變量佔用的存儲空間 | 32位機 4 字節 |
指針算術運算
p + n:地址增加 n * sizeof(*p) 位組
p++ 移動到下一個同類型對象
- 指針減法:
p2 - p1 返回元素個數差(ptrdiff_t),要求同一數組
- 非法:指針相加、指針乘除、指針與整數乘除
經典陷阱:二級指針算術
char *p = "hello";
char **pp = &p;
pp++; // pp 增加 sizeof(char*) = 4/8 字節
// *pp 現在指向 p 後面的未知內存,解引用導致 UB
運算符 & 與 *
&a:類型在原類型前加 *,值為 a 的地址
*p:類型為 p 的指向類型,左值(可賦值),佔用 p 指向的內存
- 多級遞推:
**pp 先 *pp 得到 char*,再 * 得到 char
指針與數組
- 數組名
a 在表達式中轉換為 &a[0](指向首元素指針)
- 例外:
sizeof(a)、&a、字符串字面量初始化
a[i] ⇔ *(a+i) ⇔ *(i+a) ⇔ i[a]
- 二維數組:
int a[3][4] → a 類型 int(*)[4],a+1 跳過一行(16 字節)
指針與結構體
struct S { int a, b, c; };
struct S s = {1,2,3};
struct S *p = &s;
p->a; // 等價於 (*p).a,標準寫法
// 錯誤示範:按整數指針訪問
int *pi = (int*)&s;
pi[1]; // 可能訪問到填充字節,因結構體可能有對齊填充!
指針類型轉換
(NewType*)ptr:生成新指針,類型改為 NewType*,指向類型為 NewType,地址不變
- 安全原則:
sizeof(原指向類型) >= sizeof(新指向類型) 相對安全(如 int*→char*)
- 危險:
char*→int* 可能導致未對齊訪問、越界讀寫
- 整數↔指針:
(uintptr_t)ptr、(void*)addr(C99 stdint.h),需確保地址合法
指針安全紅線
- 未初始化:
int *p; *p = 1; → 野指針,UB
- 釋放後使用:
free(p); *p = 1; → 懸垂指針
- 越界訪問:數組下標超出範圍、指針算術超出對象邊界
- 類型懲罰:
float f; int *p = (int*)&f; *p = 0; 違反嚴格別名規則,編譯器優化導致邏輯錯誤
- 棧地址外逃:返回局部變量地址
最佳實踐清單
- 聲明即初始化:
int *p = NULL; 或 = &var;
- 使用
const 保護不可修改內容:const char *p、char *const p
- 動態內存:
malloc 後檢查 NULL,free 後置 NULL
- 函數參數傳遞指針時註明所有權與生命週期
- 啟用編譯器警告:
-Wall -Wextra -Wpedantic -fsanitize=address