王剑编程网

分享专业编程知识与实战技巧

C语言进阶教程:函数指针与回调函数

函数指针是C语言中一个强大且灵活的特性,它允许我们将函数像数据一样进行传递和操作。回调函数则是函数指针的一种典型应用场景,它允许一个底层函数在特定事件发生时调用一个由上层代码提供的函数。

1. 函数指针 (Function Pointers)

定义

函数指针是一个指向函数的指针变量。正如普通指针变量可以存储变量的内存地址一样,函数指针存储的是函数的入口地址(即函数代码在内存中的起始位置)。

声明语法

声明一个函数指针需要指定它所指向的函数的返回类型和参数列表。

返回类型 (*指针变量名)(参数类型1, 参数类型2, ...);

关键点

  • 指针变量名 外面的括号 (* ... ) 是必需的。如果没有这个括号,例如 返回类型 *指针变量名(参数列表);,这会被编译器解释为一个返回 返回类型* 指针的普通函数声明。

示例:

 // 声明一个指向 "返回int,接受两个int参数" 的函数的指针 fp
 int (*fp)(int, int);
 
 // 声明一个指向 "无返回值,接受一个char*参数" 的函数的指针 print_func
 void (*print_func)(char*);
 
 // 声明一个指向 "无返回值,无参数" 的函数的指针 action_func
 void (*action_func)(void);

初始化与赋值

可以将一个符合签名的函数名(即函数地址)赋值给函数指针。

 #include <stdio.h>
 
 // 目标函数
 int add(int a, int b) {
     return a + b;
 }
 
 int subtract(int a, int b) {
     return a - b;
 }
 
 void greet(char *name) {
     printf("Hello, %s!\n", name);
 }
 
 int main() {
     int (*operation_ptr)(int, int); // 声明函数指针
     void (*display_ptr)(char*);
 
     // 初始化/赋值
     operation_ptr = add; // 将 add 函数的地址赋给 operation_ptr
                          // 也可以写成 operation_ptr = &add; &是可选的
 
     // 通过函数指针调用函数
     int result = operation_ptr(5, 3); // 等价于 add(5, 3)
                                      // 也可以写成 (*operation_ptr)(5, 3); *是可选的
     printf("Result of add: %d\n", result); // 输出: Result of add: 8
 
     operation_ptr = subtract;
     result = operation_ptr(10, 4);
     printf("Result of subtract: %d\n", result); // 输出: Result of subtract: 6
 
     display_ptr = greet;
     display_ptr("Alice"); // 输出: Hello, Alice!
 
     return 0;
 }

注意

  • 在给函数指针赋值时,函数名本身就代表函数的地址,所以 & 运算符是可选的。
  • 在通过函数指针调用函数时,直接使用 指针名(参数) 的形式即可,* 运算符也是可选的((*指针名)(参数))。现代C编译器通常都支持这两种形式。

函数指针作为函数参数

函数指针最常见的用途之一是作为另一个函数的参数。这允许我们将一个函数的行为传递给另一个函数,使其更加通用和灵活。

 #include <stdio.h>
 
 // 目标操作函数
 int add(int x, int y) { return x + y; }
 int multiply(int x, int y) { return x * y; }
 
 // 接受函数指针作为参数的函数
 // 'op' 是一个函数指针,指向一个接受两个int并返回int的函数
 int calculate(int a, int b, int (*op)(int, int)) {
     return op(a, b);
 }
 
 int main() {
     int num1 = 10, num2 = 5;
 
     int sum = calculate(num1, num2, add);      // 传递 add 函数
     int product = calculate(num1, num2, multiply); // 传递 multiply 函数
 
     printf("Sum: %d\n", sum);         // 输出: Sum: 15
     printf("Product: %d\n", product); // 输出: Product: 50
 
     return 0;
 }

函数指针作为函数返回值

虽然不那么常见,但函数也可以返回一个函数指针。

 #include <stdio.h>
 
 int add(int a, int b) { return a + b; }
 int subtract(int a, int b) { return a - b; }
 
 // 声明一个返回函数指针的函数
 // get_operation 返回一个指向 "接受两个int参数,返回int" 的函数的指针
 int (*get_operation(char opcode))(int, int) {
     switch (opcode) {
         case '+':
             return add;
         case '-':
             return subtract;
         default:
             return NULL; // 或返回一个默认操作
     }
 }
 
 int main() {
     int (*chosen_op)(int, int);
 
     chosen_op = get_operation('+');
     if (chosen_op != NULL) {
         printf("10 + 5 = %d\n", chosen_op(10, 5)); // 输出: 10 + 5 = 15
     }
 
     chosen_op = get_operation('-');
     if (chosen_op != NULL) {
         printf("10 - 3 = %d\n", chosen_op(10, 3)); // 输出: 10 - 3 = 7
     }
     return 0;
 }

语法 int (*get_operation(char opcode))(int, int) 可能看起来复杂。可以使用 typedef 来简化:

 #include <stdio.h>
 
 // 使用 typedef 简化函数指针类型声明
 typedef int (*binary_operation_func_ptr)(int, int);
 
 int add(int a, int b) { return a + b; }
 int subtract(int a, int b) { return a - b; }
 
 // 现在返回类型更清晰
 binary_operation_func_ptr get_operation_typedef(char opcode) {
     switch (opcode) {
         case '+': return add;
         case '-': return subtract;
         default:  return NULL;
     }
 }
 
 int main() {
     binary_operation_func_ptr op_func;
 
     op_func = get_operation_typedef('+');
     if (op_func) {
         printf("7 + 3 = %d\n", op_func(7, 3));
     }
     return 0;
 }

函数指针数组

可以创建函数指针数组,用于根据索引选择并调用不同的函数。

 #include <stdio.h>
 
 void say_hello() { printf("Hello!\n"); }
 void say_goodbye() { printf("Goodbye!\n"); }
 void say_thanks() { printf("Thanks!\n"); }
 
 int main() {
     // 声明并初始化一个函数指针数组
     void (*greetings[])(void) = {say_hello, say_goodbye, say_thanks};
 
     greetings[0](); // 调用 say_hello()
     greetings[1](); // 调用 say_goodbye()
     greetings[2](); // 调用 say_thanks()
 
     int choice;
     printf("Enter choice (0-Hello, 1-Goodbye, 2-Thanks): ");
     scanf("%d", &choice);
 
     if (choice >= 0 && choice < 3) {
         greetings[choice]();
     } else {
         printf("Invalid choice.\n");
     }
 
     return 0;
 }

2. 回调函数 (Callback Functions)

定义

回调函数是一种通过函数指针调用的函数。其核心思想是:

  1. 上层代码(调用者)定义一个函数(回调函数)。
  2. 上层代码将这个回调函数的地址(即函数指针)传递给一个底层代码(被调用者或库函数)。
  3. 底层代码在执行过程中,当某个特定事件发生或某个条件满足时,会使用接收到的函数指针来“回调”上层代码提供的那个函数。

这是一种事件驱动或异步编程的常见模式,允许库或底层模块在不直接了解上层具体实现的情况下,通知上层或请求上层执行特定操作。

为什么使用回调函数?

  • 通用性与可扩展性:库函数可以设计得更通用,因为它不需要知道将来会被如何使用。用户可以通过提供不同的回调函数来定制库的行为。
  • 解耦:上层代码和底层代码之间的耦合度降低。底层代码只依赖于回调函数的接口(签名),而不依赖于其具体实现。
  • 事件处理:在GUI编程、异步I/O、中断处理等场景中广泛使用,用于响应用户输入、数据到达、定时器超时等事件。

示例:排序中的比较函数

C标准库中的 qsort() 函数就是一个典型的使用回调函数的例子。qsort() 用于对数组进行排序,但它本身不知道如何比较数组中元素的“大小”,因为元素可以是任何类型(int, float, struct 等)。因此,qsort() 要求调用者提供一个比较函数作为回调。

 #include <stdio.h>
 #include <stdlib.h> // For qsort
 
 // 回调函数:用于比较两个整数
 // qsort 要求比较函数返回:
 //   < 0 如果 a < b
 //   = 0 如果 a == b
 //   > 0 如果 a > b
 int compare_integers(const void *a, const void *b) {
     int int_a = *(const int*)a;
     int int_b = *(const int*)b;
 
     if (int_a < int_b) return -1;
     if (int_a > int_b) return 1;
     return 0;
     // 或者更简洁: return (int_a > int_b) - (int_a < int_b);
     // 或者: return int_a - int_b; (对于整数,这通常有效,但要注意溢出)
 }
 
 // 回调函数:用于比较两个浮点数 (降序)
 int compare_floats_desc(const void *a, const void *b) {
     float float_a = *(const float*)a;
     float float_b = *(const float*)b;
 
     if (float_a < float_b) return 1;  // 注意这里是1,实现降序
     if (float_a > float_b) return -1; // 注意这里是-1
     return 0;
 }
 
 void print_array_int(int arr[], int size) {
     for (int i = 0; i < size; i++) {
         printf("%d ", arr[i]);
     }
     printf("\n");
 }
 
 void print_array_float(float arr[], int size) {
     for (int i = 0; i < size; i++) {
         printf("%.2f ", arr[i]);
     }
     printf("\n");
 }
 
 int main() {
     int numbers[] = {50, 20, 80, 10, 60, 30, 70, 40, 90, 0};
     int n_numbers = sizeof(numbers) / sizeof(numbers[0]);
 
     printf("Original integer array: ");
     print_array_int(numbers, n_numbers);
 
     // 使用 qsort 和回调函数 compare_integers 进行排序
     qsort(numbers, n_numbers, sizeof(int), compare_integers);
 
     printf("Sorted integer array (ascending): ");
     print_array_int(numbers, n_numbers);
 
     float values[] = {3.14, 1.0, 2.71, 0.5, 5.0};
     int n_values = sizeof(values) / sizeof(values[0]);
 
     printf("Original float array: ");
     print_array_float(values, n_values);
 
     qsort(values, n_values, sizeof(float), compare_floats_desc);
 
     printf("Sorted float array (descending): ");
     print_array_float(values, n_values);
 
     return 0;
 }

在这个例子中:

  • qsort() 是底层库函数。
  • compare_integerscompare_floats_desc 是上层代码提供的回调函数。
  • qsort() 在排序过程中,会反复调用我们提供的比较函数来确定元素的相对顺序。

示例:自定义事件处理器

假设我们正在编写一个库,它会监控某个事件,并在事件发生时通知用户。

 #include <stdio.h>
 #include <unistd.h> // For sleep (on POSIX systems)
 
 // 定义回调函数的类型
 typedef void (*event_handler_t)(int event_code, const char *message);
 
 // 库函数:注册事件处理器并模拟事件发生
 void register_and_monitor_event(event_handler_t handler) {
     printf("Library: Monitoring for events...\n");
     
     // 模拟一些工作
     sleep(2); // 暂停2秒
 
     // 模拟事件1发生
     printf("Library: Event 101 occurred! Calling handler.\n");
     if (handler != NULL) {
         handler(101, "Critical system alert!"); // 调用回调函数
     }
 
     sleep(1);
 
     // 模拟事件2发生
     printf("Library: Event 202 occurred! Calling handler.\n");
     if (handler != NULL) {
         handler(202, "User logged in."); // 再次调用回调函数
     }
     printf("Library: Monitoring finished.\n");
 }
 
 // 上层代码:定义具体的回调函数
 void my_event_logger(int code, const char *msg) {
     printf("--- User Callback Logger ---\n");
     printf("Event Code: %d\nMessage: %s\n", code, msg);
     printf("--- End Log ---\n\n");
 }
 
 void my_simple_notifier(int code, const char *msg) {
     printf("*** User Notification: Event %d - %s ***\n\n", code, msg);
 }
 
 int main() {
     printf("Main: Registering my_event_logger as callback.\n");
     register_and_monitor_event(my_event_logger);
 
     printf("\nMain: Registering my_simple_notifier as callback.\n");
     register_and_monitor_event(my_simple_notifier);
 
     printf("Main: Registering NULL as callback (no action).\n");
     register_and_monitor_event(NULL);
 
     return 0;
 }

在这个例子中:

  • register_and_monitor_event 是库函数,它接受一个 event_handler_t 类型的函数指针。
  • my_event_loggermy_simple_notifier 是用户定义的、符合 event_handler_t 签名的回调函数。
  • 库函数在“事件发生”时,调用用户注册的回调函数,将事件信息传递给它。

3. typedef与函数指针

typedef 可以极大地简化函数指针的声明和使用,提高代码的可读性。

// 不使用 typedef
void process_data(int data, void (*error_handler)(int, const char*));

// 使用 typedef
typedef void (*ErrorHandlerFunc)(int, const char*);
void process_data_typedef(int data, ErrorHandlerFunc handler);

第二种方式显然更易读。

总结

  • 函数指针是存储函数地址的变量,允许像操作数据一样操作函数。
  • 它们可以被赋值、传递给函数、从函数返回,以及存储在数组中。
  • 回调函数是通过函数指针调用的函数,是实现代码解耦、事件驱动和库函数定制化的强大机制。
  • 使用 typedef 可以使函数指针的声明和使用更加清晰。

理解并熟练运用函数指针和回调函数是C语言进阶的重要一步,它们为编写灵活、可扩展和模块化的程序提供了有力的工具。然而,也需要注意函数指针的类型匹配,以及在回调中避免引入不必要的复杂性或循环依赖。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言