指針是C語言的靈魂,也是最大難點。本文從聲明解讀、四大核心屬性、指針運算、表達式、與數組/結構體/函數的交互、類型轉換、安全陷阱八個維度,系統構建指針知識體系,助你徹底攻克指針。
從變量名出發,按運算符優先級結合:
| 聲明 | 解讀步驟 | 含義 |
|---|---|---|
int *p; | p → * → int | 指向 int 的指針 |
int p[3]; | p → [3] → int | 含3個 int 的數組 |
int *p[3]; | p → [3] → * → int | 含3個「指向 int 指針」的數組 |
int (*p)[3]; | p → * → [3] → int | 指向「含3個 int 的數組」的指針 |
int **p; | p → * → * → int | 指向「指向 int 指針」的指針(二級指針) |
int (*p)(int); | p → * → (int) → int | 指向「接受int參數、返回int」函數的指針 |
int *(*p(int))[3]; | p → (int) → * → [3] → * → int | 接受int參數、返回「指向含3個int指針數組的指針」的函數 |
核心口訣:「先看括號,後看右邊,再看左邊」,() 和 [] 優先級高於 *。
int *ptr 類型是 int*。決定指針自身內存佈局(32位系統佔4字節)及參與運算的規則。* 剩下的部分。如 int *ptr 指向類型是 int。決定解引用 *ptr 時編譯器如何解讀內存位模式。sizeof(指向類型)。sizeof(指針類型)。32位系統統一4字節。判斷表達式是否為左值的關鍵(有具體內存位置才能作左值)。區分「指針類型」與「指向類型」是精通指針的關鍵分水嶺。
char a[20]; int *ptr = (int*)a; ptr++; // ptr 值增加 sizeof(int) = 4 字節
ptr + n:地址增加 n * sizeof(指向類型) 字節。int *p = arr; p += 5; 移動 5 個 int 單元(20字節),而非 5 字節。結果為指針的表達式即指針表達式。若結果指針已有確定內存位置(如變量、解引用結果),則為左值,可放在賦值號左邊。
int a, b; int *pa=&a, *pb=&b, **pptr=&pa; &a; // 右值(臨時地址值) *ptr = &b; // *ptr 是左值(相當於 pa),可賦值 pa++; // pa 是左值,可自增
arr 在多數上下文中退化為指向首元素的指針(類型 T*),但 sizeof(arr) 仍得整個數組大小。arr[i] 等價於 *(arr+i),&arr[i] 等價於 arr+i。char *str[3] = {"hello","world","!"};,str 類型 char**,*str 指向第一串。const char[],不可修改,賦給 char* 需加 const。int (*ptr)[10] = &arr; 指向整個二維數組(或一維數組),ptr+1 跨越一整行。struct MyStruct { int a,b,c; } ss={20,30,40};
struct MyStruct *ptr = &ss;
ptr->a; // 等價於 (*ptr).a,推薦用 ->
錯誤示範: int *p = (int*)&ss; *p, *(p+1), *(p+2) 訪問成員。風險: 結構體可能因對齊插入填充字節,導致 p+1 落在填充區而非 b。務必用結構體指針訪問成員。
int fun1(char*, int);
int (*pfun1)(char*, int) = fun1;
int a = (*pfun1)("abc", 3); // 調用
int fun(char *s) { // 統計字符串ASCII和
int sum=0;
while(*s) sum += *s++;
return sum;
}
char str[]="hello";
fun(str); // 實際傳遞 str 首地址,形參 s 複製該地址,各自佔獨立棧空間
函數內對形參指針自增(s++)不影響實參指針 str。
float f=12.3; float *fptr=&f; int *p; p = (int*)&f; // 強制轉換:新指針類型 int*,指向類型 int,地址不變
(TYPE*)ptr 生成新指針,原指針不變。unsigned int addr = 0x08000000; int *ptr = (int*)addr; // 將整數視作地址 unsigned int val = (unsigned int)ptr; // 取出地址值
前提: 整數必須代表合法地址,否則解引用導致崩潰。嵌入式寄存器映射常用此技法。
// 陷阱1:類型不匹配導致越界寫入 char s='a'; int *ptr=(int*)&s; *ptr=1298; // 寫入4字節,破壞相鄰3字節! // 陷阱2:指針運算超出對象邊界 char a; int *ptr=&a; ptr++; *ptr=115; // 寫入未知內存區! // 陷阱3:轉換後訪問超出原對象範圍 // 原則:轉換後指向類型尺寸 ≤ 原對象尺寸 → 安全 // 轉換後指向類型尺寸 > 原對象尺寸 → 危險(越界讀寫)
黃金法則:
NULL/one-past-end)。NULL,防野指針。const 修飾不可修改的指向內容。指針四屬性(類型、指向類型、值、自身內存)是核心心法;運算以指向類型尺寸為單位;數組名退化規則、結構體對齊陷阱、函數指針聲明調用、轉換安全原則,構成工程級指針能力。調試時善用 %p 打印地址、watchpoint 監視內存變化、AddressSanitizer 捕獲越界。指針不難,難在時刻心中有「對象邊界」與「類型契約」。