【C语言-指针-函数指针】4. 函数指针——指针不光能指数据,还能指代码

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 4
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

前面咱们把指针和数组、指针和字符串都聊完了。你发现没有,指针这东西,指来指去,指的都是数据——数组里的元素、字符串里的字符,全是内存里存的那些0和1。

但今天我要告诉你一个更神奇的事儿:指针不光能指向数据,还能指向函数。

啥意思?函数不是代码吗?代码也能被指针指?

没错。在C语言里,函数在编译之后也是一段机器指令,存在内存里。既然存在内存里,它就有地址。既然有地址,那指针就能存它。

这就是函数指针——一个专门用来存函数地址的指针。

先回忆一下普通指针长啥样

int a = 10;
int *p = &a;    // p是指向int变量的指针

p存的是a的地址,通过*p可以访问a的值。

那函数指针呢?它存的是函数的地址,通过它可以调用那个函数。

函数指针怎么定义?

先看格式,别被吓到:

返回值类型 (*指针变量名)(参数类型列表);

举个例子。假设你有一个函数:

int add(int a, int b) {
    return a + b;
}

这个函数接受两个int,返回一个int。那指向它的函数指针就这么定义:

int (*p)(int, int);

拆开看:

  • int 是函数的返回值类型

  • (*p) 是指针变量,名字叫p,注意括号不能省

  • (int, int) 是这个函数接受的参数类型

然后把函数的地址赋给它:

p = add;      // 或者 p = &add;

注意:函数名本身就代表函数的地址,所以add和&add是一样的。跟数组名一个道理——数组名是首元素地址,函数名是函数的地址。

赋值之后,你就可以通过p来调用这个函数了:

int result = p(3, 5);   // 等价于调用 add(3, 5)

你甚至可以直接用(*p)(3, 5),效果一样。但一般咱们写p(3, 5)更简洁。

完整例子:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*p)(int, int);   // 定义一个函数指针
    p = add;              // 让它指向add函数
    
    int result = p(3, 5); // 通过指针调用函数
    printf("%d\n", result);   // 输出8
    
    return 0;
}

你看,p就是个指针,只不过它指的不是数据,是函数。你调用p(3,5),实际上就是调用了add(3,5)。

为啥要搞这么个东西?直接用函数名不行吗?

行,当然行。但函数指针有一个特别大的好处:你可以把函数当参数传给另一个函数。

这就是回调函数的思维。我给你举个例子你就明白了。

假设你要写一个排序函数,能对数组排序。但是排序的时候,到底是从小到大排还是从大到小排?你希望调用者来定,而不是写死在排序函数里。

怎么办?把“怎么比较”这件事做成一个函数,然后把函数的指针传进来。

#include <stdio.h>

// 从小到大比较
int ascending(int a, int b) {
    return a > b;   // a大于b时返回真,表示要交换
}

// 从大到小比较
int descending(int a, int b) {
    return a < b;   // a小于b时返回真,表示要交换
}

// 冒泡排序,第三个参数是一个函数指针
void bubble_sort(int arr[], int n, int (*compare)(int, int)) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (compare(arr[j], arr[j + 1])) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

int main() {
    int a[6] = {5, 2, 8, 1, 9, 3};
    
    bubble_sort(a, 6, ascending);
    for (int i = 0; i < 6; i++) printf("%d ", a[i]);
    printf("\n");   // 输出 1 2 3 5 8 9
    
    bubble_sort(a, 6, descending);
    for (int i = 0; i < 6; i++) printf("%d ", a[i]);
    printf("\n");   // 输出 9 8 5 3 2 1
    
    return 0;
}

你看,bubble_sort本身不关心你怎么比,它只知道“你来告诉我怎么比”。你传ascending进去,它就从小到大排;传descending进去,就从大到小排。排序逻辑写一遍,比较逻辑可以随便换。

这就是函数指针最大的威力——把函数当数据一样传来传去,让代码的灵活性直接上一个台阶。

函数指针的几种写法

咱们把常见的函数指针写法捋一遍:

// 1. 指向无参数、无返回值的函数
void (*p1)();

// 2. 指向有一个int参数、返回int的函数
int (*p2)(int);

// 3. 指向有两个int参数、返回int的函数
int (*p3)(int, int);

// 4. 指向有两个int参数、返回float的函数
float (*p4)(int, int);

// 5. 指向有两个int参数、返回指向int的指针的函数
int* (*p5)(int, int);

看着复杂,其实就是把函数的“签名”抄下来,把函数名换成(*指针名),参数只留类型,就这么简单。

函数指针的经典应用:菜单系统

函数指针在实际开发里用得特别多,最典型的就是菜单系统。你写一个程序,有多个功能选项,用户输入数字选择功能。用函数指针可以把“功能列表”做成一个数组。

#include <stdio.h>

void func1() { printf("执行功能1\n"); }
void func2() { printf("执行功能2\n"); }
void func3() { printf("执行功能3\n"); }

int main() {
    // 函数指针数组
    void (*menu[3])() = {func1, func2, func3};
    
    int choice;
    printf("请输入1-3选择功能:");
    scanf("%d", &choice);
    
    if (choice >= 1 && choice <= 3) {
        menu[choice - 1]();   // 调用对应的函数
    } else {
        printf("无效选择\n");
    }
    
    return 0;
}

这个例子特别有代表性。你用menu数组存了三个函数的地址,用户选1就调menu[0](),选2就调menu[1]()。以后想增加功能,只需要在数组里加一个函数指针就行了,主逻辑不用动。

函数指针做参数——回调函数的本质

咱们再来看一个例子,这次是把函数指针当参数传,实现一个“计算器”:

#include <stdio.h>

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 calculate(int a, int b, int (*op)(int, int)) {
    return op(a, b);
}

int main() {
    printf("%d\n", calculate(10, 5, add));   // 15
    printf("%d\n", calculate(10, 5, sub));   // 5
    printf("%d\n", calculate(10, 5, mul));   // 50
    return 0;
}

calculate这个函数本身不关心是加是减还是乘,它只负责“把a和b交给op指向的函数去算”。你传不同的函数进去,它就算出不同的结果。这就是回调函数——你调用calculate,calculate回头调用你传进去的那个函数。

几个常见的坑

坑1:忘了写括号

int *p(int, int);      // 错误理解:这是个函数声明,不是函数指针
int (*p)(int, int);    // 正确:这才是函数指针

int *p(int, int)会被编译器理解成“p是一个函数,接收两个int,返回int*”。因为()的优先级比*高,p(int,int)先结合,说明p是个函数,前面的*说明它返回指针。

int (*p)(int, int)因为加了括号,*先跟p结合,说明p是个指针,后面的(int, int)说明它指向的是一个函数。这就是括号的重要性。

坑2:函数指针的类型必须匹配

int add(int a, int b) { return a + b; }

int main() {
    void (*p)();     // 指向无参无返回值的函数
    p = add;         // 类型不匹配!但有些编译器只给警告
    p();             // 调用方式不对,参数传不进去
    return 0;
}

函数指针的类型必须跟它指向的函数完全一致——返回值类型、参数个数、参数类型,一个都不能差。对不上虽然有时候能编译,但运行起来行为是不可预测的。

坑3:函数指针没初始化就调用

int (*p)(int, int);
p(3, 5);   // 危险!p指向未知地址,程序崩溃

跟普通指针一样,用之前一定要让它指向一个合法的函数。

坑4:把函数指针跟返回指针的函数搞混

int* func(int, int);    // func是一个函数,返回int*
int (*p)(int, int);     // p是一个指针,指向返回int的函数

这俩长得像,但意思完全不同。一个是函数,一个是指针。分清楚的关键是看*跟谁挨着。

总结一下

函数指针就一句话:它是一个指针,存的是函数的地址,通过它你可以调用那个函数。

定义格式:返回值类型 (*指针名)(参数类型列表);

核心用法:

  • 把函数当参数传给另一个函数(回调)

  • 把函数存进数组,做成“功能表”

  • 在运行时动态决定调用哪个函数

用的时候记住两条:

  • 函数名本身就是地址,赋值的时候直接写函数名就行,不用加&。

  • 通过函数指针调用函数,写p(参数)或者(*p)(参数)都行,前者更简洁。

函数指针是C语言里“把函数当数据用”的核心工具。你现在可能觉得有点绕,但多写几个例子就顺了。