20、指针与函数:指针作为函数参数(传址调用),函数指针,回调函数

指针和函数,这两个东西单独拎出来都不算简单。把它们放在一起,很多人就开始头疼了。说实话,我当年学C语言的时候,也是在这个地方卡了很久。但后来我发现,只要搞清楚了三个核心场景——传址调用函数指针回调函数——这块就算彻底拿下了。

今天咱们就一个一个来拆解。别急,慢慢来。

一、指针作为函数参数:传址调用

先问一个问题:C语言里,函数参数默认是怎么传递的?

答案是值传递。也就是说,你把一个变量扔进函数,函数拿到的是它的副本。你在函数里改来改去,改的都是副本,原变量纹丝不动。

那如果我想在函数里修改外面的变量呢?

嗯,这时候就需要传址调用了——把变量的地址传进去,函数通过地址直接操作那块内存。

核心区别:

  • 值传递:函数内修改不影响外部
  • 传址调用:函数内修改直接影响外部

看个最经典的例子——交换两个数:

#include <stdio.h>

// 错误示范:值传递
void swap_wrong(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // 这里改的是副本,外面不受影响
}

// 正确示范:传址调用
void swap_right(int *pa, int *pb) {
    int temp = *pa;
    *pa = *pb;
    *pb = temp;
    // 通过指针直接操作原变量
}

int main() {
    int x = 10, y = 20;

    swap_wrong(x, y);
    printf("值传递后:x=%d, y=%d\n", x, y);  // 还是 10, 20

    swap_right(&x, &y);
    printf("传址调用后:x=%d, y=%d\n", x, y);  // 变成 20, 10

    return 0;
}

我在项目中遇到过好几次这样的bug——新手写交换函数,明明逻辑看起来没问题,但就是换不过来。原因就是忘了传地址。

我的习惯:只要函数需要修改外部变量,参数一律用指针。哪怕只是可能修改,我也用指针。这样接口语义更清晰,调用方一看就知道这个参数可能会被改。

传址调用的另一个常见场景是函数返回多个值。C语言函数只能有一个返回值,但你可以通过指针参数“带出来”多个结果。

// 计算商和余数,通过指针返回
int divide(int dividend, int divisor, int *quotient, int *remainder) {
    if (divisor == 0) {
        return -1;  // 错误
    }
    *quotient = dividend / divisor;
    *remainder = dividend % divisor;
    return 0;  // 成功
}

二、函数指针:把函数当成变量

函数指针,说白了就是指向函数的指针。函数本身也有地址,存在代码区。我们可以用一个指针变量来存这个地址,然后通过指针调用函数。

你想想看,函数名本身就是一个地址——函数的入口地址。所以:

// 声明一个函数指针
// 指向:返回值是 int,参数是两个 int 的函数
int (*func_ptr)(int, int);

// 让指针指向某个函数
func_ptr = &add;   // & 可以省略,函数名本身就是地址

// 通过指针调用函数
int result = func_ptr(3, 5);   // 等价于 add(3, 5)

函数指针的声明语法有点绕。我个人的记忆方法是:先写一个普通函数声明,然后把函数名换成 (*指针名)

普通函数声明 对应的函数指针声明
int add(int a, int b); int (*add_ptr)(int, int);
void print_msg(char *msg); void (*print_ptr)(char *);
double calc(double x, double y); double (*calc_ptr)(double, double);

注意:函数指针的括号不能少。int *func(int, int) 是返回指针的函数,不是函数指针。我曾经在代码审查时看到有人把这两个搞混,结果编译报错找了半天。

三、回调函数:把函数作为参数传递

回调函数,其实就是把函数指针作为另一个函数的参数。这样,被调用的函数可以在适当的时候“回调”你传入的函数。

为什么要这么做?说白了,就是解耦。你写一个通用函数,但具体做什么事情,让调用方来决定。

最经典的例子就是qsort——C标准库的快速排序函数。它需要一个比较函数的指针,由你来决定怎么比。

#include <stdio.h>
#include <stdlib.h>

// 比较函数:升序
int compare_asc(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

// 比较函数:降序
int compare_desc(const void *a, const void *b) {
    return (*(int*)b - *(int*)a);
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 传入比较函数指针作为回调
    qsort(arr, n, sizeof(int), compare_asc);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    // 输出:1 2 3 5 8 9

    return 0;
}

你看,qsort 本身不知道你要升序还是降序。它把比较逻辑留给你,通过回调函数来实现。这就是回调的威力。

我在项目中用回调最多的场景是事件驱动。比如写一个按键驱动模块:

// 定义回调函数类型
typedef void (*key_callback_t)(int key_code);

// 注册回调
void key_register_callback(key_callback_t cb);

// 在中断或轮询中调用回调
void key_scan() {
    int key = read_key();
    if (key != KEY_NONE) {
        // 调用注册的回调函数
        key_callback(key);
    }
}

这样,按键检测的逻辑和按键处理逻辑就分开了。上层只需要注册一个函数,底层检测到按键就自动回调。耦合度大大降低。

四、知识体系总览

下面这张图,把本章的核心逻辑串起来了。建议你多看几遍,理解指针和函数之间的三种关系。

指针与函数:三大核心关系 指针 + 函数 传址调用 指针作为函数参数 修改外部变量 返回多个值 函数指针 指向函数的指针 存储函数入口地址 通过指针调用函数 回调函数 函数指针作为参数 解耦调用方与实现 事件驱动、qsort等 核心思想:指针让函数更灵活 传址调用 → 修改数据 | 函数指针 → 存储函数 | 回调 → 传递行为

五、避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

我曾经犯过的错:

  • 函数指针类型不匹配:声明了一个 int (*)(int, int) 的指针,却指向了 void func(int, int)。编译能过,但运行时会出奇怪的问题。一定要确保返回值类型和参数类型完全一致。
  • 回调函数中修改了不该改的数据:在回调里不小心改了全局变量,导致其他模块行为异常。回调函数应该尽量只处理自己的逻辑,不要产生副作用。
  • 传址调用时忘了判空:如果传入的指针是 NULL,直接解引用就崩了。我建议在函数入口处加一句 if (ptr == NULL) return;

我的建议:

  • typedef 给函数指针起个别名,代码会清晰很多。比如 typedef int (*cmp_func_t)(const void*, const void*);
  • 回调函数的注册和注销要成对出现。注册了不注销,容易造成悬空指针。
  • 多读标准库的源码,比如 qsortbsearchsignal 等,看看它们是怎么用回调的。

指针和函数的组合,是C语言从入门到进阶的一道坎。跨过去,你会发现后面的世界豁然开朗。好了,今天就聊到这里。多写代码,多调试,慢慢就熟了。


公众号:蓝海资料掘金营,微信deep3321