理解c语言中的回调函数

来看看维基百科中如何定义回调函数

在计算机程序设计中,回调函数,或简称回调,是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。

这段话不是那么好理解,不同语言实现回调的方式有些许不同。其实可以这样理解,回调就是 在一个函数中调用另外一个函数

c 语言中,回调是使用函数指针来实现的。 函数指针——顾名思义,是指向一个函数的指针。通常函数指针有两个方面的用途,一个是转换表(jump table),另一个是作为参数传递给一个函数

下面是两个函数指针的声明

1
2
int (*f)(int, float);
int *(*g[])(int, float);

前者把 f 声明为一个函数指针,它所指的函数接受两个参数,分别是一个整型值和浮点型值,并返回一个整型值。后者把 g 声明为一个数组,数组的元素类型是一个函数指针,它所指向的函数接受两个参数,分别是一个整型值和浮点型值,并返回一个整型指针。

需要注意的是,简单声明一个函数指针并不意味着它马上就可以使用。和其他指针一样,对函数指针执行间接访问之前必须把它初始化为指向某个函数。下面的代码段说明了一种初始化函数指针的方法。

1
2
int f(int);
int (*pf)(int) = f;

第 2 个声明创建了函数指针 pf,并把它初始化为指向函数 f。函数指针的初始化也可以通过一条赋值语句来完成。在函数指针的初始化之前具有 f 的原型是很重要的,否则编译器就无法检查 f 的类型是否与 pf 所指向的类型一致。


通过一个例子简单介绍回调函数的使用

大家应该都对 c 语言的库函数 qsort 有所了解,qsort 声明如下

1
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))

可以看到,它的第三个参数是一个函数指针,传入两个没有定义指针指向的类型的参数ab,返回一个整型值。实际上这里使用了回调函数。通过回调函数,qsort 可以在运行时调用用户定义的函数(底层代码调用在高层定义的子程序)。

这里我们设计一个简单的 sort 函数,来理解回调过程

1、定义函数指针

1
typedef int (*compar)(const int *a, const int *b);

2、自定义 sort 函数,为了简单,这里使用冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int *sort(int *nums, int n, compar cmp) {
int *target = malloc(n*sizeof(int));

if (!target) perror("Memory error");

memcpy(target, num, n * sizeof(int));

for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
if (cmp(target[i], target[j]) > 0) {
target[i] ^= target[j] ^= target[i] ^= target[j];
}
}
}
return target;

}

3、实现函数回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

typedef int (*compar)(const int *a, const int *b);

// 定义实现回调函数的"调用函数"
int *sort(int *nums, int n, compar cmp) {
int *target = malloc(n*sizeof(int));

if (!target) perror("Memory error");

memcpy(target, num, n * sizeof(int));

for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
if (cmp(target[i], target[j]) <= 0) {
target[i] ^= target[j] ^= target[i] ^= target[j];
}
}
}
return target;

}

// 定义回调函数
int cmp1(int a, int b) {
return a < b;
}
int main(int argc, char const *argv[])
{
int a[10] = {1, 4, 3, 1, 10, 4, 5};
int *x = bubble_sort(a, 7, cmp1);
for (int i = 0; i < 7; i++)
printf("%d ", x[i]);
printf("\n");

return 0;
}

运行结果:

1
1 1 3 4 4 5 10

调用函数向其函数中传递 int (*compar)(const int *a, const int *b);这是 int cmp1(int a, int b) 函数的入口地址,即 PC 指针可以通过移动到该地址执行int cmp1(int a, int b)函数,可以通过类比数组来理解。

实现函数调用中,函数调用了“调用函数”,再在其中进一步调用被“调用函数”。相比于主函数直接调用“被调函数”,这种方法为使用者,而不是开发者提供了灵活的接口。另外,函数入口可以像变量一样设定同样为开发者提供了灵活性。


题外话(Kotlin中使用接口实现回调)

Android开发中,也经常会遇到回调的使用。由于Kotlin语言没有函数指针的概念,实现回调的话可以通过接口来实现。在这里,我们声明一个接口,而不是一个函数指针,这个接口有一个方法,当被调用方完成它的任务时,这个方法将被调用。

通过一个例子来说明这一点:

假如我们要实现这个这样一个功能。

当前有一个MainActivity和一个LocalFileFragment,点击MainActivity中的全选按钮能够让LocalFileFragment改变全选状态。

实现步骤如下:

  1. LocalFileFragment中创建接口
  2. LocalFileFragment中声明接口
  3. MainActivity中实现这个接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class LocalFileFragment: Fragment() {
// 声明接口
private var mListener: OnFileCheckedListener? = null

// 定义接口
interface OnFileCheckedListener {
fun changeStatus()
}

// 绑定接口实现
fun setOnFileCheckedListener(listener: OnFileCheckedListener) {
mListener = listener
}

private fun doSomething() {
if (...) mListener.changeStatus()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity: AppCompatActivity() {
// 实现回调接口
private val mListener = object : LocalFileFragment.OnFileCheckedListener {
fun changeStatus() {
Log.d("MainActivity", "Change Status!")
}
}

private fun doSomething() {
val mLocalFileFragment = LocalFileFragment()

mLocalFileFragment.setOnFileCheckedListener(mListener)
}
}
文章作者: Sshpark
文章链接: http://sshpark.com.cn/2019/03/22/对回调的一些理解/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sshpark