指针的5层境界,从入门到劝退:C语言指针进阶全解析
学C语言绕不开指针。指针入门的时候,你知道了*和&,知道了指针就是存地址的变量。
但这只是开始。
当你遇到函数指针、指针数组、二级指针、回调函数......才真正体会到什么叫"从入门到劝退"。
今天从低到高,拆解指针的5层境界。每一层都附代码和图解,看懂了,指针这块你就通关了。
第1层:指针数组 vs 数组指针------名字差不多,东西完全不同
这是最容易搞混的一对,面试必考。
名字看起来差不多,但本质完全不同。区分方法就一个:看谁先和变量名结合。
c
int *p[5]; // 指针数组
int (*p)[5]; // 数组指针
怎么区分?
[] 的优先级高于 *,所以:
int *p[5]:p 先和[5]结合 → p 是数组 ,数组里每个元素是int*→ 指针数组int (*p)[5]:p 先和*结合 → p 是指针 ,指向一个有 5 个 int 的数组 → 数组指针
指针数组用来干嘛?
存放多个地址,最常见的用法是管理一组字符串:
c
// 指针数组:每个元素是一个 char*,指向一个字符串
char *fruits[3] = {"apple", "banana", "orange"};
for (int i = 0; i < 3; i++) {
printf("%s\n", fruits[i]);
}
💡 记忆口诀 :先结合 [] 就是数组(指针数组),先结合 * 就是指针(数组指针)。
第2层:二级指针------什么时候需要"指向指针的指针"
一级指针能改数据,二级指针能改指向。
先看一个问题:
c
// ❌ 错误:想在函数里给指针分配内存
void allocate(int *p) {
p = (int *)malloc(sizeof(int)); // 改的是副本,外面感知不到
}
int main() {
int *ptr = NULL;
allocate(ptr);
// ptr 还是 NULL!因为函数里改的是副本
}
问题出在哪?C 语言是值传递,函数拿到的是指针的副本。你改副本的指向,原指针纹丝不动。
正确写法:传指针的地址,也就是二级指针
c
// ✅ 正确:用二级指针修改外部指针的指向
void allocate(int **pp) {
*pp = (int *)malloc(sizeof(int)); // 通过 *pp 修改的是原始指针
**pp = 42;
}
int main() {
int *ptr = NULL;
allocate(&ptr); // 传 ptr 的地址
printf("%d\n", *ptr); // 输出 42
free(ptr);
}
💡 核心逻辑:
- 想改
int a→ 传int * - 想改
int *p→ 传int ** - 规则永远不变:想改谁,就传谁的地址
二级指针的实际场景
最典型的就是链表头节点操作:
c
// 在链表头部插入节点,需要修改 head 指针
void insert_head(Node **head, int data) {
Node *new_node = (Node *)malloc(sizeof(Node));
new_node->data = data;
new_node->next = *head; // 新节点指向原头节点
*head = new_node; // 修改 head 指向新节点
}
// 调用
Node *head = NULL;
insert_head(&head, 10);
insert_head(&head, 20);
// 链表:20 -> 10 -> NULL
💡 记住:凡是需要在函数里"改变一个指针的指向",就用二级指针。
第3层:函数指针------函数也有地址
函数名就是地址,就像数组名一样。
c
int add(int a, int b) {
return a + b;
}
add 这个函数名,本身就是函数在内存中的入口地址。所以你可以用指针来存它:
c
// 定义一个函数指针,指向"接收两个 int,返回 int"的函数
int (*fp)(int, int) = add;
// 通过指针调用函数
int result = fp(3, 5); // 等价于 add(3, 5)
printf("%d\n", result); // 输出 8
函数指针的写法怎么看?
拆解 int (*fp)(int, int):
(*fp)→ fp 是指针- 指针指向什么?看右边
(int, int)→ 一个函数,参数是两个 int - 再看左边
int→ 这个函数返回 int
💡 用 typedef 简化,不然太长了:
c
// 给函数指针类型起个别名
typedef int (*CalcFunc)(int, int);
// 用起来就清爽了
CalcFunc fp = add;
printf("%d\n", fp(3, 5));
第4层:回调函数------函数指针作为参数
这才是函数指针真正厉害的地方:让函数变得"可插拔"。
标准库里的 qsort 就是一个经典例子。它不关心你怎么比较大小,你告诉它就行:
c
// 你的比较函数
int compare_int(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
int main() {
int arr[] = {5, 2, 8, 1, 9, 3};
int n = 6;
// qsort 不关心比较逻辑,你传什么函数,它就怎么排
qsort(arr, n, sizeof(int), compare_int);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]); // 输出:1 2 3 5 8 9
}
}
自己写一个回调,更好理解
c
typedef int (*MathOp)(int, int);
// 这个函数不关心具体做加减乘除,你传什么就做什么
int calculate(int a, int b, MathOp op) {
return op(a, b);
}
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 main() {
printf("%d\n", calculate(10, 3, add)); // 13
printf("%d\n", calculate(10, 3, sub)); // 7
printf("%d\n", calculate(10, 3, mul)); // 30
}
💡 回调的本质:把"做什么"交给调用者决定,框架代码只关心"怎么做"。这是 C 语言实现解耦的核心手段。
第5层:函数指针数组------转移表实战
把函数指针放进数组,用下标代替 switch,代码瞬间清爽。
还是那个计算器例子,先看普通写法:
c
// ❌ 普通写法:一堆 switch-case
switch (op) {
case 1: result = add(a, b); break;
case 2: result = sub(a, b); break;
case 3: result = mul(a, b); break;
case 4: result = divide(a, b); break;
}
用函数指针数组(转移表):
c
// ✅ 转移表写法:把函数地址存进数组,用下标直接调用
typedef int (*CalcFunc)(int, int);
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 divide(int a, int b) { return b ? a / b : 0; }
// 函数指针数组:下标 0~3 对应 4 个函数
CalcFunc ops[] = { NULL, add, sub, mul };
// ops[1] = add
// ops[2] = sub
// ops[3] = mul
int main() {
int a = 10, b = 3;
// 不用 switch,直接用下标调用
printf("10 + 3 = %d\n", ops[1](a, b)); // 13
printf("10 - 3 = %d\n", ops[2](a, b)); // 7
printf("10 * 3 = %d\n", ops[3](a, b)); // 30
}
💡 转移表的优势:
- 消除 switch-case 的冗余代码
- 新增操作只需往数组里加一个函数,不用改逻辑
- 用下标 O(1) 查找,比 switch 更高效
附加:字符指针与常量字符串的隐藏陷阱
一个看起来没问题的代码,可能藏着段错误。
c
// ❌ 危险:修改常量字符串
char *str = "hello";
str[0] = 'H'; // 段错误!"hello"是常量字符串,只读
"hello" 存储在程序的只读数据段 ,str 只是指向它的指针。试图修改只读内存,程序直接崩。
正确写法:要修改就拷贝一份
c
// ✅ 用数组:内容存在栈上,可以修改
char str[] = "hello";
str[0] = 'H'; // 没问题,现在 str 是 "Hello"
// ✅ 或者动态拷贝
char *str = strdup("hello"); // malloc + strcpy
str[0] = 'H';
free(str);
💡 记住 :char *str = "hello" 里的 "hello" 是常量,不能改。char str[] = "hello" 里的 "hello" 会被拷贝到栈上,可以改。
写在最后
指针的5层境界,从简单到复杂:
- 指针数组 vs 数组指针:看谁先和变量名结合
- 二级指针:想改指针的指向,就传它的地址
- 函数指针:函数名就是地址,指针可以存它
- 回调函数:把"做什么"交给调用者
- 函数指针数组:转移表消除 switch-case
它们的关系是一层套一层的:
指针 → 指针数组 → 指向指针的指针 → 指向函数的指针 → 函数指针数组
每一层都是在前一层的基础上多绕一个弯。弯多了容易晕,但只要把每一步拆开看,其实就那么回事。
C语言的指针不会让你失望的,前提是你别跳过它。