函数指针与回调

在C语言中,函数名本质上是一个指向函数代码的指针(函数入口地址)。函数指针允许我们将函数作为参数传递、存储在数组中、或者在运行时动态决定调用哪个函数。 回调函数是通过函数指针实现的一种设计模式,允许我们将用户自定义的行为注入到通用算法中。 函数指针是实现多态、事件驱动编程、库函数扩展的重要手段。本章将系统讲解函数指针的声明、使用及回调函数的典型应用。

📌 函数指针的声明与使用

函数指针的声明形式为:返回类型 (*指针名)(参数类型列表);。注意括号不能省略,否则变成返回指针的函数。

#include <stdio.h>

// 定义一个普通函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 声明一个函数指针,指向返回int、接收两个int参数的函数
    int (*funcPtr)(int, int);

    // 将函数地址赋给指针(函数名就是地址)
    funcPtr = add;

    // 通过函数指针调用函数(两种写法等效)
    int result1 = funcPtr(3, 5);
    int result2 = (*funcPtr)(3, 5);

    printf("结果: %d\n", result1);  // 输出8

    return 0;
}
注意: 函数指针的类型必须与所指向的函数完全匹配(返回类型和参数类型列表)。可以使用 typedef 简化函数指针类型的声明。

🔸 使用 typedef 简化函数指针

#include <stdio.h>

// 定义函数指针类型别名
typedef int (*Operation)(int, int);

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

int main() {
    Operation op = add;
    printf("add: %d\n", op(10, 5));

    op = sub;
    printf("sub: %d\n", op(10, 5));

    return 0;
}

📞 函数指针作为函数参数(回调)

将函数指针作为参数传递给另一个函数,被调用的函数就是回调函数。这种模式使得通用函数可以处理不同的具体行为。

#include <stdio.h>

// 回调函数类型
typedef void (*Callback)(int);

// 通用函数,接收回调
void processArray(int arr[], int n, Callback cb) {
    for (int i = 0; i < n; i++) {
        cb(arr[i]);   // 调用回调函数处理每个元素
    }
}

// 具体的回调函数
void printSquare(int x) {
    printf("%d ", x * x);
}

void printDouble(int x) {
    printf("%d ", x * 2);
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    int n = sizeof(nums) / sizeof(nums[0]);

    printf("平方: ");
    processArray(nums, n, printSquare);
    printf("\n");

    printf("两倍: ");
    processArray(nums, n, printDouble);
    printf("\n");

    return 0;
}

📚 标准库中的回调:qsort

C语言标准库中的 qsort 函数是回调的经典应用,它通过比较函数指针实现对任意类型数组的排序。

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

// 比较整数(升序)
int cmpInt(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    return ia - ib;   // 升序
    // return ib - ia; // 降序
}

// 比较浮点数(注意精度)
int cmpFloat(const void *a, const void *b) {
    float fa = *(const float *)a;
    float fb = *(const float *)b;
    if (fa < fb) return -1;
    if (fa > fb) return 1;
    return 0;
}

// 比较字符串(按字典序)
int cmpString(const void *a, const void *b) {
    const char **sa = (const char **)a;
    const char **sb = (const char **)b;
    return strcmp(*sa, *sb);
}

int main() {
    int intArr[] = {34, 7, 23, 32, 5, 62};
    int n = sizeof(intArr) / sizeof(intArr[0]);
    qsort(intArr, n, sizeof(int), cmpInt);
    printf("排序后的整数: ");
    for (int i = 0; i < n; i++) printf("%d ", intArr[i]);
    printf("\n");

    float floatArr[] = {3.14, 1.41, 2.71, 0.58};
    n = sizeof(floatArr) / sizeof(floatArr[0]);
    qsort(floatArr, n, sizeof(float), cmpFloat);
    printf("排序后的浮点数: ");
    for (int i = 0; i < n; i++) printf("%.2f ", floatArr[i]);
    printf("\n");

    const char *strArr[] = {"banana", "apple", "cherry", "date"};
    n = sizeof(strArr) / sizeof(strArr[0]);
    qsort(strArr, n, sizeof(const char *), cmpString);
    printf("排序后的字符串: ");
    for (int i = 0; i < n; i++) printf("%s ", strArr[i]);
    printf("\n");

    return 0;
}
注意: qsort 的比较函数需要返回负数、0或正数来表示小于、等于或大于。参数类型为 const void *,使用时需要先转换为实际类型。

📋 函数指针数组

可以将多个函数指针存入数组,实现类似多态的分发机制。常见于状态机、菜单系统等。

#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 divide(int a, int b) { return b != 0 ? a / b : 0; }

int main() {
    // 函数指针数组,存放四个运算函数
    int (*ops[])(int, int) = {add, sub, mul, divide};
    const char *names[] = {"加法", "减法", "乘法", "除法"};

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

🛠️ 实战:实现自己的通用排序函数

模仿 qsort,实现一个简单的冒泡排序通用版本,使用回调函数比较元素。

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

// 通用冒泡排序
void bubbleSort(void *base, size_t nmemb, size_t size,
                int (*compar)(const void *, const void *)) {
    char *arr = (char *)base;  // 按字节操作
    for (size_t i = 0; i < nmemb - 1; i++) {
        for (size_t j = 0; j < nmemb - i - 1; j++) {
            // 获取第j个和第j+1个元素的指针
            void *elem1 = arr + j * size;
            void *elem2 = arr + (j + 1) * size;
            if (compar(elem1, elem2) > 0) {
                // 交换两个元素
                char temp[size];
                memcpy(temp, elem1, size);
                memcpy(elem1, elem2, size);
                memcpy(elem2, temp, size);
            }
        }
    }
}

// 比较整数
int intCmp(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr) / sizeof(arr[0]);

    bubbleSort(arr, n, sizeof(int), intCmp);

    printf("排序结果: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

🎯 回调函数传递额外数据

有时回调函数需要访问外部数据,可以通过 void * 参数传递上下文。

#include <stdio.h>

// 回调函数类型,接收一个额外上下文参数
typedef void (*ProcessFunc)(int, void *);

// 遍历数组,为每个元素调用回调,并传递上下文
void processArray(int arr[], int n, ProcessFunc func, void *ctx) {
    for (int i = 0; i < n; i++) {
        func(arr[i], ctx);
    }
}

// 回调:计算累加和
void sumCallback(int val, void *ctx) {
    int *sum = (int *)ctx;
    *sum += val;
}

// 回调:打印并输出倍数(额外参数为倍数)
void printMultipleCallback(int val, void *ctx) {
    int multiplier = *(int *)ctx;
    printf("%d ", val * multiplier);
}

int main() {
    int nums[] = {1, 2, 3, 4, 5};
    int n = sizeof(nums) / sizeof(nums[0]);

    int sum = 0;
    processArray(nums, n, sumCallback, &sum);
    printf("总和: %d\n", sum);

    int factor = 3;
    printf("乘以 %d: ", factor);
    processArray(nums, n, printMultipleCallback, &factor);
    printf("\n");

    return 0;
}

🔍 复杂函数指针声明解析

函数指针的声明有时会很复杂,下面是一些常见形式的解析技巧。

  • int (*p)(int, int); —— p是指向函数的指针,该函数返回int,接收两个int。
  • int (*arr[5])(int); —— arr是一个数组,包含5个函数指针,每个指针指向返回int、接收一个int的函数。
  • int (*func(int, int))(double); —— func是一个函数,它返回一个指向函数的指针,该返回的函数接收double返回int。
  • int (*(*p)(int))(double); —— p是指针,指向一个函数,该函数接收int,返回一个指向函数的指针,该返回的函数接收double返回int。

解析规则:从最左边的标识符开始,先看括号内的部分,按照“右左法则”理解。

建议: 实际开发中,遇到复杂声明应使用 typedef 进行分层定义,提高可读性。

⚠️ 常见错误与注意事项

  • 函数指针类型不匹配:将返回类型或参数类型不匹配的函数赋值给指针,可能导致未定义行为。编译器通常会警告。
  • 忘记函数名不加括号funcPtr = add; 正确;funcPtr = add(); 是调用函数,将返回值赋给指针,类型错误。
  • 调用时忘记参数:通过函数指针调用时,参数列表必须与声明一致。
  • 使用未初始化的函数指针:未初始化的函数指针指向随机地址,调用会导致崩溃。
  • 混淆函数指针与函数原型:函数原型是声明,函数指针是变量,注意区分。
  • 强制转换函数指针:虽然可以强制转换,但必须确保转换后的类型与实际调用一致,否则行为未定义。

📋 综合示例:插件化计算器

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

// 定义操作函数类型
typedef double (*Operation)(double, double);

// 具体操作
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double divi(double a, double b) { return b != 0 ? a / b : 0; }

// 操作映射
typedef struct {
    char op;
    Operation func;
} OpMapping;

OpMapping ops[] = {
    {'+', add},
    {'-', sub},
    {'*', mul},
    {'/', divi}
};
const int opCount = sizeof(ops) / sizeof(ops[0]);

Operation getOperation(char op) {
    for (int i = 0; i < opCount; i++) {
        if (ops[i].op == op) return ops[i].func;
    }
    return NULL;
}

int main() {
    double a, b;
    char op;

    printf("输入表达式 (如 3.5 + 2.1): ");
    scanf("%lf %c %lf", &a, &op, &b);

    Operation func = getOperation(op);
    if (func != NULL) {
        printf("结果: %.2f\n", func(a, b));
    } else {
        printf("无效运算符\n");
    }

    return 0;
}

函数指针是C语言实现灵活性和扩展性的重要工具,尤其在库设计、框架开发中不可或缺。 通过回调机制,我们可以将通用算法与具体业务解耦。掌握函数指针,将为后续学习函数式编程思想、高级数据结构打下坚实基础。