5.3 指针与函数:传址、回调与函数指针


5.3 指针与函数:传址、回调与函数指针

本节摘要:C 只有值传递,"传址"传的是地址值的副本——但有了地址,被调函数就能越过栈帧边界访问调用者的内存。输出参数、swap、scanf 的地址参数全部建立在这个机制上。更进一步,函数代码本身也有地址,函数指针让"调用哪个函数"变成可以传递的数据,这是回调、事件驱动与 qsort 排序框架的机器基础。本节顺带收编 void 指针与 const 指针的工程约束。

传址:一条跨栈帧的隧道

第 4 章证明过:值传递改不了实参,因为形参只是栈帧里的副本。解法不是"换一种传递方式",而是把变量的地址这个值传过去

#include <stdio.h> void swap(int *a, int *b) /* 收到的是两个地址 */ { int t = *a; /* 顺地址去 main 的帧里读 */ *a = *b; /* 写回 main 的帧 */ *b = t; } int main(void) { int x = 3, y = 5; swap(&x, &y); printf("%d %d\n", x, y); /* 5 3 */ return 0; }

swap 的帧里只有两个指针(16 字节),真正的数据在 main 的帧里。指针是跨帧访问合法内存的签证——签证指向谁家的活内存,就能读写谁家。

输出参数模式是它的标准化应用。C 函数只能返回一个值,多结果就这样带出:

/* 返回状态码,结果走指针带出:C 标准库的惯用签名 */ int div_safe(int a, int b, int *quot, int *rem) { if (b == 0) return -1; /* 失败:输出参数不动 */ *quot = a / b; *rem = a % b; return 0; }

回看 scanf:scanf("%d", &n) 传地址正是为了让它把转换结果写进你的变量——第 2 章的规则在机器层面对齐了。

数组参数是传址的天然场景(5.2 节:数组名退化成首地址),函数拿到首地址与长度即可遍历——注意"长度必须另传"的铁律在所有数组参数函数上生效。

函数指针:代码地址也是地址

函数编译后住在代码段,入口有一个地址。&func 或直接写 func 都得到这个地址(函数名在表达式里同样"退化"成地址)。存它的变量就是函数指针:

#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int main(void) { int (*op)(int, int); /* 指向“两个int参数返回int”的函数的指针 */ op = add; printf("%d\n", op(10, 3)); /* 13 */ op = sub; printf("%d\n", op(10, 3)); /* 7 */ return 0; }

声明读法:int (*op)(int, int)——op 是指针(括号保证先结合星号),指向"参数两个 int、返回 int"的函数。调用哪个函数在运行期才决定——这是从"写死的跳转"到"数据驱动的跳转"的跃迁。

最有说服力的应用是标准库的 qsort:

#include <stdio.h> #include <stdlib.h> int cmp_int(const void *a, const void *b) { int x = *(const int *)a; int y = *(const int *)b; return (x > y) - (x < y); /* 通用三态比较,避免溢出 */ } int main(void) { int arr[] = {42, 7, 19, 3, 88}; qsort(arr, 5, sizeof(int), cmp_int); /* 把比较规则当数据传入 */ for (int i = 0; i < 5; i++) printf("%d ", arr[i]); printf("\n"); return 0; }

qsort 不知道你要按什么规则排序,它只持有比较函数的地址,每需要比较就通过指针调用。算法骨架与比较策略解耦——面向对象语言里的多态、图形界面的按钮点击处理、操作系统的中断处理表,机器层面全是同一招:把代码地址放进数据结构,运行期查表调用。

图 2 回调机制的地址链路

图 2 回调机制的地址链路

void 指针与 const 指针:两条工程约束

void 指针是"没有滤镜的纯地址":任何类型指针可无警告转入 void 指针,反过来必须显式转换。qsort 的比较函数收到的就是 void 指针——它不能解引用、不能做运算,只能转回具体类型再用。"通用容器"的接口全靠它搭桥。代价是类型安全后移到调用者手上:转错类型不报错、跑起来才崩。qsort 每次比较都要做转换与间接调用,性能敏感场景常被手写特化排序替代——通用性的代价。

const 指针是契约声明,三级强度递增:

const int *p; /* 指向 const int:内容不可通过 p 改(指针可移动) */ int *const q; /* const 指针:指针本身不可改指向(内容可改) */ const int *const r; /* 双 const:都不可改 */

读法口诀:const 在星号左边修饰内容,右边修饰指针本身。它的价值在接口签名上——size_t strlen(const char *s) 向调用者承诺"我只读不写你的字符串",编译器替你监督这个承诺。"参数不需要写就加 const" 是大型 C 工程的通行纪律,Linux 内核代码里俯拾皆是。

⚠️ 常见坑:函数指针类型不匹配。声明 int (*op)(int,int) 却赋给它一个 long(*)(int,int) 的函数,多数编译器只给警告;通过错误的类型调用,参数按错误宽度传递,栈帧错乱。函数指针赋值必须类型严格一致。

💡 关键直觉:指针让"数据"与"行为"都变成可传递的东西——数据传地址,行为传函数地址。整个事件驱动编程世界就建在这两根柱子上。

本节要点回顾

  • C 只有值传递:传址传的是地址副本,但足以让被调函数访问调用者的活内存。
  • 输出参数模式:状态码作返回值、结果走指针,是 C 接口设计的标准姿势。
  • 函数指针 = 代码地址变量:运行期决定调用目标,qsort 回调是活教材。
  • void 指针是纯地址:转换责任归调用者;const 三级强度是接口契约,不需要写就加 const。
  • 函数指针类型必须严格匹配:警告不是噪音。

下一章先去数组与字符串的地盘(第 6 章),再回来攻下指针篇的最后堡垒:堆与动态内存。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U