🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

函数指针 — 把函数当参数传递

C语言学习教程

函数指针 — 把函数当参数传递

函数指针会让你从”能用 C 写程序”升级到”能设计灵活的系统”。别怕——如果你理解了基础指针的核心思想,函数指针只是把”指向数据”换成了”指向函数”。

为什么要有函数指针?

你要写一个排序函数,用户有时升序、有时降序、有时按绝对值排——难道为每种逻辑写一个排序函数?那会爆炸。

更好的思路:把”比较规则”传给排序函数。 函数也在内存中有地址,所以——函数也有地址,也能用指针指向!

数据 → 有地址 → 数据指针指向数据
函数 → 也有地址 → 函数指针指向函数
          完全一样的道理!

核心比喻:菜谱与书签

函数       = 一本菜谱(指令序列,放在内存)
函数地址   = 菜谱在书架上的位置编号
函数指针   = 一个书签,写着菜谱的位置编号
调用函数   = 按书签找到菜谱,照着做

这就是”运行时决定行为”: 跑到那一行时才知道今天执行什么。

基本语法

语法看起来吓人,但规则简单:把函数声明中的函数名换成 (*指针名)

int add(int a, int b);       // 普通函数声明

int (*fp)(int a, int b);     // 函数指针声明
// int              → 返回类型
//     (*fp)        → fp 是指针(括号是必须的!)
//           (int,int) → 参数列表

// 没有括号是什么?
int *fp(int a, int b);       // 这是返回 int* 的普通函数!不是指针!

括号决定一切: (*fp) 是指针,没有括号就是函数。

示例一:基本函数指针——从声明到调用

#include <stdio.h>

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

int main() {
    int (*operation)(int, int);  // 声明函数指针

    operation = add;             // 指向 add(函数名就是地址)
    printf("%d\n", operation(10, 5));  // 15

    operation = subtract;        // 换指向 subtract
    printf("%d\n", operation(10, 5));  // 5

    return 0;
}

内存图解——函数也在内存中有”房子”:

代码段(只读内存):
地址:    0x4000            0x4100
      ┌──────────────┐ ┌──────────────┐
      │ add 的指令... │ │ subtract指令  │
      └──────────────┘ └──────────────┘

数据段:
地址:    0x2000
      ┌──────────────┐
      │ operation    │  存 0x4000 (add 的地址)
      │ (函数指针)   │  如果 =subtract,就变 0x4100
      └──────────────┘

operation(10, 5) 的执行:
1. 读 operation → 0x4000
2. 跳到 0x4000 执行
3. 传参数 10, 5 → 返回 15

示例二:函数指针数组——计算器菜单

把多个函数指针放进数组,获得一个”函数菜单”:

#include <stdio.h>

int add(int a, int b)      { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b)   { return b ? a / b : 0; }

int main() {
    int (*ops[])(int, int) = {add, subtract, multiply, divide};
    char *names[] = {"加法", "减法", "乘法", "除法"};

    int a = 20, b = 5;
    printf("=== 计算器 ===\n");

    for (int i = 0; i < 4; i++) {
        printf("%d. %s: %d\n", i+1, names[i], ops[i](a, b));
    }

    // 模拟用户选第 2 个
    int choice = 2;
    printf("用户选了%s,结果: %d\n", names[choice-1], ops[choice-1](a, b));

    return 0;
}

“函数菜单”结构:

ops 数组:
索引:    ops[0]      ops[1]      ops[2]      ops[3]
      ┌──────────┐┌──────────┐┌──────────┐┌──────────┐
      │  0x4000  ││  0x4100  ││  0x4200  ││  0x4300  │ 函数地址
      └────┬─────┘└────┬─────┘└────┬─────┘└────┬─────┘
           ▼          ▼          ▼          ▼
       ┌──────┐  ┌──────┐  ┌──────┐  ┌──────┐
       │ add  │  │ sub  │  │ mul  │  │ div  │
       └──────┘  └──────┘  └──────┘  └──────┘

ops[2](20, 5) → 读 ops[2]=0x4200 → 跳 multiply → 返回 100

好处:加新运算只需写一个新函数并加到数组,其他代码一行不用改。

示例三:qsort——标准库的函数指针应用

qsort 第四个参数是函数指针,你告诉它”怎么比”,它帮你排:

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

// 比较函数必须匹配签名:int (*)(const void*, const void*)
// void* 是"通用指针"

int cmp_asc(const void *a, const void *b) {
    return *(int*)a - *(int*)b;   // void* → int* → 取值 → 比较
}

int cmp_desc(const void *a, const void *b) {
    return *(int*)b - *(int*)a;   // 颠倒就是降序
}

int cmp_abs(const void *a, const void *b) {
    int ia = *(int*)a, ib = *(int*)b;
    return (ia<0 ? -ia : ia) - (ib<0 ? -ib : ib);
}

void print_arr(int *arr, int n) {
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    int nums[] = {42, -7, 99, -13, 56};
    int n = 5;

    printf("原始:     "); print_arr(nums, n);

    qsort(nums, n, sizeof(int), cmp_asc);
    printf("升序:     "); print_arr(nums, n);   // -13 -7 42 56 99

    qsort(nums, n, sizeof(int), cmp_desc);
    printf("降序:     "); print_arr(nums, n);   // 99 56 42 -7 -13

    qsort(nums, n, sizeof(int), cmp_abs);
    printf("绝对值:   "); print_arr(nums, n);   // -7 -13 42 56 99

    return 0;
}

qsort 调用图解:

qsort(nums, 5, sizeof(int), cmp_asc)
  │      │      │             │
  │      │      │             └── 函数指针:"怎么比大小"
  │      │      └── 每个元素 4 字节
  │      └── 5 个元素
  └── 数组首地址

qsort 内部:不需要知道数据类型,只负责按比较结果交换位置

关键洞见: qsort 的作者和你从未见面,但通过函数指针协同工作——你提供比较逻辑,他提供排序框架。这就是”把做什么和怎么做分离开”。

示例四:回调模式——for_each

回调是把一个函数传给另一个函数,后者在遍历每个元素时”回调”你:

#include <stdio.h>

// 通用遍历器——不知道做什么,只负责遍历
void for_each(int *arr, int n, void (*fn)(int *)) {
    for (int i = 0; i < n; i++) fn(&arr[i]);
}

// 各种回调函数
void double_it(int *x) { *x *= 2; }
void add_five(int *x)  { *x += 5; }
void print_it(int *x)  { printf("%d ", *x); }
void square_it(int *x) { *x = (*x) * (*x); }

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int n = 5;

    printf("原始:  "); for_each(arr, n, print_it); printf("\n");  // 1 2 3 4 5
    for_each(arr, n, double_it);
    printf("加倍:  "); for_each(arr, n, print_it); printf("\n");  // 2 4 6 8 10
    for_each(arr, n, square_it);
    printf("平方:  "); for_each(arr, n, print_it); printf("\n");  // 4 16 36 64 100

    return 0;
}

代码复用度对比:

不用函数指针:5 个操作 × n 行重复的 for 循环
用函数指针  :1 个 for_each × 5 个操作(每个一行)

示例五:插件/策略系统

不修改主程序,通过注册函数指针扩展功能:

#include <stdio.h>
#include <string.h>

typedef void (*FilterFunc)(int *data, int size);

void filter_grayscale(int *d, int s) { printf("  [灰度滤镜]\n"); }
void filter_sepia(int *d, int s)     { printf("  [复古滤镜]\n"); }
void filter_invert(int *d, int s)    { printf("  [反色滤镜]\n"); }

struct FilterEntry {
    char name[32];
    FilterFunc func;
};

int main() {
    struct FilterEntry filters[] = {
        {"灰度", filter_grayscale},
        {"复古", filter_sepia},
        {"反色", filter_invert}
    };
    int count = 3, image[100] = {0};

    printf("=== 选择滤镜 ===\n");
    for (int i = 0; i < count; i++)
        printf("%d. %s\n", i+1, filters[i].name);

    int choice = 3;
    printf("选择: %d\n", choice);
    if (choice >= 1 && choice <= count)
        filters[choice-1].func(image, 100);  // 通过函数指针调用

    return 0;
}

架构: 主程序遍历滤镜表 → 用户选择 → 查表 → 调用对应函数。新加滤镜只需写新函数并加入数组。

示例六:状态机

每个状态是一个函数,返回下一个状态:

#include <stdio.h>

typedef int (*StateFunc)(void);

int state_idle(void)    { printf("[空闲] → "); return 1; }
int state_running(void) { printf("[运行] → "); return 2; }
int state_paused(void)  { printf("[暂停] → "); return 1; }
int state_stop(void)    { printf("[停止]\n"); return -1; }

int main() {
    StateFunc states[] = {state_idle, state_running, state_paused, state_stop};

    int current = 0;
    for (int s = 0; s < 6 && current != -1; s++)
        current = states[current]();

    if (current != -1) states[3]();  // 强制结束
    return 0;
}

状态转换: idle → running → paused → running → paused → running → stop

五大常见错误

错误一:忘记括号——声明成返回指针的函数

int *fp(int, int);      // ✘ 这是返回 int* 的函数!
int (*fp)(int, int);    // ✔ 这才是函数指针

记忆法:(*名字) 是指针,没有括号就是函数。

错误二:返回值类型不匹配

int add(int a, int b) { return a + b; }
void (*fp)(int, int);   // ✘ 要求返回 void,但 add 返回 int
int (*fp)(int, int);    // ✔ 匹配

错误三:参数个数或类型错误

int (*fp)(int, int) = add;
fp(10, 20, 30);   // ✘ 三个参数
fp(10);           // ✘ 一个参数
fp(10, 20);       // ✔

错误四:忘记 NULL 检查

void process(int (*fn)(int)) {
    int result = fn(42);      // ✘ fn 可能是 NULL → 崩溃
}
// ✔ 先检查:
if (fn == NULL) return;

错误五:typedef 没用好反而更乱

typedef int (*MathFunc)(int, int);  // "MathFunc"看起来像函数名
typedef int (*BinOp)(int, int);     // ✔ 清晰的名字

用 typedef 简化(强烈推荐)

// 不用 typedef——每次都要写又长又丑的声明
int (*funcs[10])(int, int);

// 用 typedef——起好名字
typedef int (*BinOp)(int, int);  // BinOp = 二元操作函数指针
BinOp funcs[10];    // 秒懂
BinOp p = add;      // 秒懂

// 经典例子
typedef int (*CompareFunc)(const void*, const void*);
qsort(arr, n, sizeof(int), (CompareFunc)cmp_asc);

函数指针速查

写法含义
int (*fp)(int)声明函数指针
fp = myFunc赋值(函数名是地址)
fp(42)通过指针调用
void (*fp)(void)无参数无返回值
int (*fp[])(int,int)函数指针数组

重点回顾

  1. 函数也有地址——函数名就是函数地址(和数组名一样)
  2. (*fp) 是关键——括号把 * 和名字绑在一起
  3. 三大场景——qsort、回调、策略/插件系统
  4. typedef 是好朋友——让函数指针类型像普通类型
  5. 类型严格匹配——返回值、参数一个都不能差