C语言指针的三种进阶形态
| 概念 | 写法 | 本质 | 类比 |
|---|---|---|---|
| 指向指针的指针(二层指针) | int **pp | 一个变量,里面存的是另一个指针变量的地址 | “门牌号本子的存放抽屉号” |
| 指针数组 | int *arr[10] | 一个数组,里面每个元素都是指针 | “一排门牌号本子” |
| 函数指针 | void (*f)(int) | 一个变量,里面存的是函数的入口地址 | “记录修鞋铺位置的纸条” |
1.int **pp(指向指针的指针)
你有一张纸条(p),上面写着"101室"(&a)。
你又拿了一张纸条(pp),上面写着"放p的那个抽屉的编号"。
所以*pp取出的是p(101室),**pp取出的是a的值。
int a = 5; int *p = &a; // p存的是a的地址 int **pp = &p; // pp存的是p的地址 printf("%p\n", *pp);//输出p的地址 printf("%d\n", **pp); // 输出5嵌入式里什么时候用:
- 函数要修改外部指针本身(如分配内存后改变指针指向)。
- 操作二维动态数组(行指针数组)。
//函数要修改外部指针指针示例: //让函数 alloc_memory 把 malloc 申请到的内存地址“带出来”,赋给外部的指针 p void alloc_memory(int **pp) { *pp = malloc(100); // 改变外部指针 p 本身 } int *p = NULL; alloc_memory(&p); // 现在 p 指向了堆上 100 字节 //操作二位动态数组示例: //用二层指针 matrix 创建一个“动态二维数组”,即行数和列数都在运行时由 rows 和 cols 决定, //而不是编译时固定。 int **matrix = malloc(rows * sizeof(int*)); for (int i = 0; i < rows; i++) { matrix[i] = malloc(cols * sizeof(int)); }2.int *arr[3](指针数组)
指针数组的目的:用一个数组统一管理多个地址,方便批量操作这些地址指向的数据。
这是一个数组,里面有3个元素,每个元素都是int*类型(指向int的指针)。
int a=1, b=2, c=3; int *arr[3] = {&a, &b, &c}; printf("%d", *arr[0]); // 输出1int main(int argc, char *argv[])argc:参数个数。argv:这是一个指针数组,每个元素指向一个字符串(命令行输入的内容)。
例如输入./a.out hello world,则:argv[0]→"./a.out"argv[1]→"hello"argv[2]→"world"
这就是典型的指针数组用法:操作系统把命令行字符串的起始地址,全部放进这个数组里传给你。
使用场景:
| 场景 | 例子 | 本质 |
|---|---|---|
| 字符串数组 | char *argv[] = {"ls", "-l", NULL}; | 每个元素指向一个字符串 |
| 函数跳转表 | void (*cmd[])(void) = {func1, func2, func3}; | 用下标直接调用不同函数 |
| 多对象管理 | int *bufs[4];每个指向不同传感器数据缓冲区 | 用循环统一处理所有对象 |
与二层指针对比:
| 写法 | 本质 | 内存 | 典型场景 |
|---|---|---|---|
int *arr[10] | 数组,里面存了10个地址 | 40字节(10个指针) | 固定数量指针,编译时确定 |
int **pp | 指针,指向一个指针变量 | 4或8字节(1个指针) | 需要动态分配行数(二维数组)或函数内修改外部指针 |
指针数组 = 多个指针的“收纳盒”,让你能用循环一次性处理所有地址。
3.void (*f)(int)(函数指针)
定义一个变量f,它指向一个“参数为int、返回值为void”的函数。
void print_num(int n) { printf("%d", n); } void (*f)(int) = print_num; // f指向print_num函数 f(5); // 调用,输出5嵌入式里什么时候用:
中断向量表:每个中断号对应一个函数地址。
回调函数:比如定时器溢出时,调用你指定的函数。
状态机跳转表:根据当前状态自动执行对应函数。
4.总结:
| 写法 | 类型 | 占内存 | 取内容后得到 |
|---|---|---|---|
int *p; | 指向int的指针 | 4字节(地址) | int值 |
int **pp; | 指向“指向int的指针”的指针 | 4字节 | int *值 |
int *arr[10]; | 有10个元素的数组,每个元素是int* | 40字节(4×10) | int值(经过两次解引用) |
void (*f)(int); | 指向函数的指针 | 4字节(地址) | 函数入口地址(调用即执行) |
题目:
请用C语言写一个程序,实现以下功能:
定义三个整数
a=10, b=20, c=30。定义一个指针数组
arr,让它的三个元素分别指向a, b, c。定义一个指向指针的指针
pp,让它指向arr的第二个元素(即指向b的那个元素)。定义一个函数指针
f,指向一个函数print_value,该函数接收一个int*指针,并打印它指向的值。用
pp取出b的值,并用f打印它。
#include <stdio.h> void print_value(int *p) { printf("value = %d\n", *p); } int main() { int a = 10, b = 20, c = 30; // 1. 指针数组:每个元素指向一个int int *arr[3] = {&a, &b, &c}; // 2. 指向指针的指针:指向arr的第2个元素(即&b) int **pp = &arr[1]; // 或者 &arr[1] 等价于 arr + 1 // 3. 函数指针 void (*f)(int*) = print_value; // 4. 通过pp取出b的值,用f打印 f(*pp); // *pp = arr[1] = &b,所以 f(&b) 打印20 return 0; }补充:三层指针
1、“一层指针”是什么
int a = 5; int *p = &a;a是一个房间,里面住了5,门牌号是0x1000。p是一个“门牌号本子”,上面写着0x1000。
一层指针 = 存地址的变量。
2、二层指针:存“门牌号本子”的“抽屉号”
int **pp = &p;pp是一个“抽屉”,里面放的不是门牌号,而是“存放门牌号本子的抽屉号”。也就是说:
pp指向的是p这个变量本身。
画出来就是:
pp (0x2000) → p (0x1000) → a (5)pp的值是0x2000(p 的地址)*pp取出的是p的值,即0x1000**pp取出的是a的值,即5
3、什么时候必须用二层指针?(核心场景)
场景1:函数里要改变“指针变量本身”
如果你写:
void set_null(int *p) { p = NULL; // 改的是 p 的副本,外部的 p 不受影响 }传进去的p是复制品,改它没用。
要让函数把外面的指针改成NULL,必须传指针的地址:
void set_null(int **pp) { *pp = NULL; // 直接改外部的指针变量 } int *p = &a; set_null(&p); // 传的是 p 的地址 // 现在 p 变成 NULL 了本质:你要改的是“本子上的内容”,但本子本身是别人的,所以你只能把“本子的存放位置”告诉函数,让它去那里改。
场景2:动态分配二维数组
int **matrix; matrix = (int **)malloc(rows * sizeof(int *)); for (int i = 0; i < rows; i++) { matrix[i] = (int *)malloc(cols * sizeof(int)); }matrix是一层指针,指向一个“指针数组”的首地址。每个
matrix[i]又是一个指针,指向一行数据。访问
matrix[1][2]时,编译器先找第1行指针,再找第2列数据。
场景3:操作系统命令行参数
int main(int argc, char **argv)argv是一个二层指针(等价于char *argv[])。它指向一个指针数组,数组里每个元素指向一个命令行字符串。
操作系统通过二层指针,把多个字符串的地址一次性传给你。
4、三层指针有没有?
有,但极少见。比如:
int ***ppp;ppp指向pp,pp指向p,p指向a。只有在三维动态数组或者非常深的嵌套修改场景才会用到,普通工程基本碰不到。
5、总结
一层指针存地址,二层指针存“存地址的变量”的地址。
需要修改指针本身时(如函数内改外部的指针、动态分配二维数组),就用二层指针。
6、题目
问:为什么swap_ptr的参数必须是int **,而不能是int *?
void swap_ptr(int **pp1, int **pp2) { int *temp = *pp1; *pp1 = *pp2; *pp2 = temp; } int a=1, b=2; int *p1 = &a, *p2 = &b; swap_ptr(&p1, &p2); // 现在 p1 指向 b,p2 指向 a
答:因为要交换的是p1和p2这两个指针变量本身,而不是它们指向的值。传int *只能交换复制品,传int **才能直接改原件。