C 语言指针深入
更新时间:2026-08-25。本文是
languages/c/主题高手层第 1 篇。入门层讲了指针是"带类型的地址",本文进入指针的"硬骨头":多级指针、函数指针、数组指针,以及能把人绕晕的复杂声明。这些是读内核代码、看懂回调、驾驭动态内存的必备功力。
本文要回答的问题
int **p是什么意思?什么时候需要用到"指针的指针"?- 函数指针怎么定义、怎么用?回调函数的本质是什么?
const int *p和int *const p有什么区别?int (*p)[5]和int *p[5]差在哪?
一、指针运算规则
指针 +1 不是地址加 1,而是加一个"步长"(所指类型的大小):
int a[3] = {10, 20, 30};
int *p = a;
p + 1; // 地址加 4(int 是 4 字节)
*(p + 1); // 20
char *q = (char *)a;
q + 1; // 地址加 1(char 是 1 字节)| 指针类型 | +1 的实际地址增量 |
|---|---|
char * | 1 字节 |
int * | 4 字节 |
double * | 8 字节 |
struct X * | sizeof(X) 字节 |
二、多级指针(指针的指针)
2.1 什么时候需要
指针的指针,本质是"指向指针变量的指针"。典型场景:函数要修改外部指针本身。
void alloc(int **pp) {
*pp = malloc(sizeof(int)); // 修改外部指针,让它指向新内存
}
int main(void) {
int *p = NULL;
alloc(&p); // 传 &p,即 int** 类型
*p = 42;
free(p);
return 0;
}如果只传 int *p(一级指针),函数内 p = malloc(...) 改的是副本,外部的 p 还是 NULL——这就是为什么需要 int **。
2.2 二级指针图示
int x = 10;
int *p = &x; // p 指向 x
int **pp = &p; // pp 指向 p
**pp; // 10,两次解引用得到 x
三、函数指针与回调
3.1 定义与使用
函数名就是函数的地址,函数指针就是存函数地址的指针:
int add(int a, int b) { return a + b; }
int (*fp)(int, int); // 函数指针:指向"接收两个 int 返回 int"的函数
fp = add; // 赋值(add 和 &add 都行)
fp(3, 4); // 调用,得到 73.2 回调函数(callback)
函数指针最常见的用途是回调——把函数当参数传给另一个函数:
#include <stdlib.h>
int cmp_int(const void *a, const void *b) {
return *(int *)a - *(int *)b;
}
int arr[] = {5, 2, 8, 1};
qsort(arr, 4, sizeof(int), cmp_int); // 把 cmp_int 作为回调传给 qsortqsort 不知道你在比较什么类型,它只负责"排序框架",具体的比较逻辑由你通过函数指针注入——这就是回调。
3.3 常见场景
qsort(排序)、bsearch(查找)等标准库函数- 信号处理:
signal(SIGINT, handler) - 事件驱动、插件机制、
atexit注册退出函数
四、const 与指针的组合
const 修饰指针时的位置决定"谁不可变":
| 声明 | 含义 |
|---|---|
const int *p | 指向的值不可改(*p = 5 错),指针本身可改 |
int *const p | 指针本身不可改(p = ... 错),指向的值可改 |
const int *const p | 两者都不可改 |
记忆口诀:const 在 * 左边,修饰的是"所指内容";在 * 右边,修饰的是"指针本身"。
const int *p1; // 内容只读
int *const p2 = &x; // 指针只读五、void * 泛型指针
void * 是"万能指针",可以指向任何类型,但不能直接解引用(因为不知道读几字节):
void *p = malloc(100); // malloc 返回 void*
int *ip = p; // 隐式转换为 int*(C 允许)
// 解引用前必须转成具体类型
int val = *(int *)p;void * 是 C 泛型编程的基础(qsort 的回调参数就是 const void *)。
六、复杂指针声明的阅读方法
6.1 数组指针 vs 指针数组(重点区分)
int *p[5]; // 指针数组:5 个 int* 的数组
int (*p)[5]; // 数组指针:指向"5 个 int 的数组"的指针括号改变了结合顺序:
int *p[5]:先[5](数组),再*(元素是指针)→ 指针数组int (*p)[5]:先*(指针),再[5](指向数组)→ 数组指针
6.2 阅读规则:从内到外,从标识符出发
int (*func[3])(int);从 func 出发:func 是数组([3])→ 元素是指针(*)→ 指向函数((int) 参数)→ 返回 int。
所以是:一个包含 3 个函数指针的数组,每个函数接收 int 返回 int。
6.3 常见复杂声明速查
| 声明 | 读法 |
|---|---|
int *p[5] | 指针数组:5 个 int* |
int (*p)[5] | 数组指针:指向 int[5] 的指针 |
int *f(void) | 函数:返回 int* |
int (*f)(void) | 函数指针:指向返回 int 的函数 |
int *(*f[3])(void) | 3 个函数指针的数组,每个返回 int* |
七、与本站主线衔接
- 函数指针与分支预测:通过函数指针调用是"间接跳转",CPU 分支预测器更难预测,见 L4 分支预测。
- void 与泛型*:
qsort的泛型机制,是理解 C++ 模板"零成本抽象"的铺垫,见 C++ 模板。 - const 与编译器优化:
const帮助编译器做优化,见 L4 编译器选项。
一句话总结
指针深入 = 三块硬骨头:多级指针(改指针本身要传二级指针)、函数指针(回调的基石)、数组指针 vs 指针数组(靠括号和"从内到外"读法区分);啃下这些,读内核源码、用 qsort、写回调都不再发怵。
上一篇:数组、字符串与结构体 下一篇:动态内存管理