指針是C語言的靈魂,也是初學者最頭疼的知識點。它像一把鋒利的刀,用得好能大幅提升代碼效率,用不好則會讓程序漏洞百出。今天這篇文章,我們從數組與指針的基礎關系講起,一步步揭開指針進階類型的神秘面紗,最后用實戰案例鞏固所學——保證通俗易懂,還會標注所有重點和坑點。
一、數組與指針:繞不開的基礎關系
1.1 數組名的本質:不是簡單的地址
很多人以為"數組名就是首元素地址",這句話對但不完整。數組名的本質有兩個例外,這是初學者最容易掉的坑!
重點結論:
- 一般情況:數組名表示數組首元素的地址。
例:int arr[10] = {1,2,...,10};
中,arr
與&arr[0]
地址相同。 - 兩個例外(必須牢記):
sizeof(數組名)
:數組名代表整個數組,計算整個數組的字節大小。
例:sizeof(arr)
結果為40
(10個int,每個4字節),而非指針大小(4/8)。&數組名
:數組名代表整個數組,取出的是整個數組的地址(與首元素地址值相同,但偏移量不同)。
例:&arr + 1
偏移40字節(跳過整個數組),而arr + 1
偏移4字節(跳過一個元素)。
實戰驗證:
#include <stdio.h>
int main() {int arr[10] = {1,2,3,4,5,6,7,8,9,10};printf("&arr[0] = %p\n", &arr[0]); // 首元素地址printf("arr = %p\n", arr); // 首元素地址(等價于上一行)printf("&arr = %p\n", &arr); // 整個數組的地址(值相同,意義不同)// 關鍵差異:+1操作printf("&arr[0]+1 = %p\n", &arr[0]+1); // 跳過1個元素(+4字節)printf("arr+1 = %p\n", arr+1); // 跳過1個元素(+4字節)printf("&arr+1 = %p\n", &arr+1); // 跳過整個數組(+40字節)return 0;
}
輸出結果解析:
前三個地址值相同,但&arr+1
會跳過整個數組(10個int,共40字節),而前兩者只跳過1個元素(4字節)。這證明&arr
指向的是整個數組,而非單個元素。
1.2 用指針訪問數組:靈活但要謹慎
有了對數組名的理解,我們可以用指針靈活訪問數組元素。核心邏輯是:數組元素的訪問本質是"首地址+偏移量"。
等價關系(重點):
arr[i]
等價于*(arr + i)
- 指針
p
指向首元素時,p[i]
等價于*(p + i)
示例代碼:
#include <stdio.h>
int main() {int arr[5] = {10,20,30,40,50};int* p = arr; // p指向首元素// 兩種訪問方式等價printf("arr[2] = %d\n", arr[2]); // 30printf("*(p+2) = %d\n", *(p + 2)); // 30printf("p[2] = %d\n", p[2]); // 30(指針也支持下標)return 0;
}
1.3 一維數組傳參:別被"數組形式"騙了
當數組作為參數傳遞給函數時,形參看似是數組,本質是指針。這也是為什么在函數內部用sizeof
求不出數組長度的原因。
- 數組傳參實際傳遞的是首元素地址,而非整個數組。
- 函數形參兩種寫法(等價):
void test(int arr[]); // 數組形式(本質是指針) void test(int* arr); // 指針形式(更直觀)
易錯點演示:
#include <stdio.h>
// 形參寫成數組形式,本質還是指針
void test(int arr[]) {printf("函數內sizeof(arr) = %d\n", sizeof(arr)); // 4或8(指針大小)
}int main() {int arr[10] = {0};printf("主函數內sizeof(arr) = %d\n", sizeof(arr)); // 40(整個數組大小)test(arr);return 0;
}
1.4.冒泡排序(指針應用)
- 核心:相鄰元素比較交換,通過指針訪問數組元素。
- 優化版(提前終止有序數組):
void bubble_sort(int* arr, int sz) {for(int i=0; i<sz-1; i++) {int flag = 1; // 假設本趟有序for(int j=0; j<sz-i-1; j++) {if(arr[j] > arr[j+1]) {int tmp = arr[j];arr[j] = arr[j+1];arr[j+1] = tmp;flag = 0; // 發生交換,無序}}if(flag) break; // 無交換,直接退出} }
二、指針的進階類型:從二級指針到數組指針
2.1 二級指針:指針的指針
指針變量也是變量,它的地址需要用"二級指針"存儲。可以理解為:一級指針指向數據,二級指針指向一級指針。
示例圖解:
int a = 10; // 數據
int* pa = &a; // 一級指針(指向a)
int** ppa = &pa; // 二級指針(指向pa)
操作邏輯:
*ppa
等價于pa
(通過二級指針獲取一級指針)**ppa
等價于*pa
等價于a
(通過二級指針獲取數據)
2.2 指針數組:存放指針的數組
指針數組是數組,其元素類型是指針。比如int* arr[5]
表示:一個有5個元素的數組,每個元素是int*
類型的指針。
用途:存儲多個同類型地址
#include <stdio.h>
int main() {int arr1[] = {1,2,3};int arr2[] = {4,5,6};int arr3[] = {7,8,9};// 指針數組存儲三個一維數組的首地址int* parr[3] = {arr1, arr2, arr3};// 訪問arr2的第2個元素(5)printf("%d\n", parr[1][1]); // 等價于*(parr[1] + 1)return 0;
}
2.3 數組指針:指向數組的指針
數組指針是指針,它指向一個完整的數組。比如int (*p)[5]
表示:一個指針,指向"有5個int元素的數組"。
易混淆對比(重點):
定義 | 本質 | 解讀 |
---|---|---|
int* p[5] | 指針數組 | 先與[] 結合,是數組,元素為int* |
int (*p)[5] | 數組指針 | 先與* 結合,是指針,指向int[5] 數組 |
數組指針的用法:
#include <stdio.h>
int main() {int arr[3][5] = {{1,2,3,4,5}, {6,7,8,9,10}, {11,12,13,14,15}};int (*p)[5] = arr; // arr是首行地址(指向第一行數組)// 訪問第二行第三列元素(8)printf("%d\n", *(*(p + 1) + 2)); // 等價于p[1][2]return 0;
}
三、字符串與字符指針:藏著坑的常量
3.1 字符指針的兩種用法
字符指針(char*
)既可以指向單個字符,也可以指向字符串的首字符。后者更常見,但要注意常量字符串的特性。
示例:
#include <stdio.h>
int main() {// 指向單個字符char ch = 'a';char* pc = &ch;// 指向字符串首字符(重點)const char* pstr = "hello"; // "hello"是常量字符串,不可修改printf("%s\n", pstr); // 打印整個字符串(從首字符開始直到'\0')return 0;
}
3.2 常量字符串的存儲:節省空間的小技巧
C/C++會把相同的常量字符串存儲在同一塊內存中,這是容易踩坑的點。
示例(面試常考):
#include <stdio.h>
int main() {char str1[] = "hello"; // 數組:開辟新空間,存儲"hello"char str2[] = "hello"; // 數組:再開辟新空間,存儲"hello"const char* str3 = "hello"; // 指針:指向常量區的"hello"const char* str4 = "hello"; // 指針:指向同一塊常量區空間printf("str1 == str2 ? %d\n", str1 == str2); // 0(地址不同)printf("str3 == str4 ? %d\n", str3 == str4); // 1(地址相同)return 0;
}
結論:
- 用常量字符串初始化數組時,每次都會開辟新空間
- 用常量字符串初始化字符指針時,多個指針可能指向同一塊空間(節省內存)
四、二維數組傳參:首行地址是關鍵
二維數組可以理解為"數組的數組"(每個元素是一維數組)。因此,二維數組的數組名表示首行的地址(即第一個一維數組的地址),類型是數組指針。
二維數組傳參的正確方式:
#include <stdio.h>
// 形參可寫成二維數組形式,或數組指針形式
void print_arr(int (*p)[5], int row, int col) {for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {printf("%d ", p[i][j]); // 等價于*(*(p+i)+j)}printf("\n");}
}int main() {int arr[3][5] = {{1,2,3,4,5}, {6,7,8,9,10}, {11,12,13,14,15}};print_arr(arr, 3, 5); // 傳遞首行地址return 0;
}
五、函數指針:讓指針指向代碼
函數也有地址(函數名就是地址),用函數指針可以存儲函數地址,實現更靈活的調用(比如回調函數)。
5.1 函數指針的定義與使用
定義格式:返回類型 (*指針名)(參數類型列表)
#include <stdio.h>
int add(int a, int b) {return a + b;
}int main() {// 定義函數指針,指向add函數int (*pf)(int, int) = add; // 等價于&add// 兩種調用方式printf("add(2,3) = %d\n", add(2,3)); // 直接調用printf("pf(2,3) = %d\n", pf(2,3)); // 用指針調用printf("(*pf)(2,3) = %d\n", (*pf)(2,3)); // 等價寫法return 0;
}
5.2 函數指針數組與轉移表:簡化多分支邏輯
函數指針數組是存儲函數指針的數組,適合實現"菜單-功能"類邏輯(如計算器),替代冗長的switch-case。
實戰案例:用函數指針數組實現計算器
#include <stdio.h>
// 四則運算函數
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }int main() {int x, y, input;// 函數指針數組(轉移表):下標1-4對應加減乘除int (*operate[5])(int, int) = {0, add, sub, mul, div};do {printf("1:加 2:減 3:乘 4:除 0:退出\n");printf("請選擇:");scanf("%d", &input);if (input >= 1 && input <= 4) {printf("輸入操作數:");scanf("%d %d", &x, &y);// 用數組下標調用對應函數printf("結果: %d\n", operate[input](x, y));} else if (input != 0) {printf("輸入錯誤!\n");}} while (input != 0);return 0;
}
優勢:
- 新增功能只需添加函數并更新數組,無需修改分支邏輯
- 代碼更簡潔,可讀性更高
六、typedef:給復雜類型起"小名"
typedef
可以為復雜類型(如指針、數組指針、函數指針)重命名,簡化代碼。但要注意與#define
的區別。
6.1 用法示例:
#include <stdio.h>
// 重命名基本類型
typedef unsigned int uint;// 重命名指針類型
typedef int* int_ptr;// 重命名數組指針
typedef int (*arr_ptr)[5]; // 指向int[5]數組的指針// 重命名函數指針
typedef int (*calc_func)(int, int); // 指向"int(int,int)"函數的指針int main() {uint a = 10; // 等價于unsigned intint_ptr p1, p2; // p1和p2都是int*(指針)arr_ptr parr; // 等價于int (*parr)[5]calc_func pf = add; // 等價于int (*pf)(int,int)return 0;
}
6.2 與#define的區別(易錯點):
#define
是簡單替換,而typedef
是真正的類型重命名。
#include <stdio.h>
typedef int* int_ptr; // 類型重命名
#define INT_PTR int* // 宏替換int main() {int_ptr p1, p2; // p1和p2都是int*(正確)INT_PTR p3, p4; // 替換后為int* p3, p4; → p4是int(錯誤)return 0;
}
七、總結與易錯點回顧
- 數組名的兩個例外:
sizeof(數組名)
和&數組名
表示整個數組 - 數組傳參本質:形參是指針,需額外傳遞長度
- 指針數組vs數組指針:前者是數組(存指針),后者是指針(指向數組)
- 常量字符串存儲:相同常量字符串可能共享內存,數組初始化則不共享
- 函數指針數組:適合實現多功能菜單,替代switch-case
- typedef與#define:typedef是類型重命名,#define是文本替換
指針雖然復雜,但只要抓住"地址"和"類型"兩個核心(地址決定指向哪里,類型決定+1跳過多少字節),就能逐步掌握。多寫代碼驗證,少死記硬背,才是學好指針的關鍵!