所谓回调函数,就是将函数指针作为参数的函数,看下面的三个案例,就知道回调函数用来干什么了。
1 打印任意类型的数据
这里只是演示回调函数,因此暂时只实现打印int、double和自定义类型的数据。假定我们要实现的函数名称为myPrint,这里需要考虑以下几个问题:
(1)如何传入数据传入数据?如果把要打印的数据当成函数的参数传入,那么参数的类型无法确定(假设函数内部是不知道要打印的数据类型),这个时候可以使用空指针,即把要打印的数据地址传入函数中myPrint;
(2)如何考虑打印?因为C语言无法像C++那样使用cout来打印,只能只用printf,即按格式打印,那么你数据传入之后,还是需要知道类型才能打印。对于这个问题,可以使用函数指针作为myPrint的参数类型,即使用回调函数,在此之前,需要为每一种类型专门写一个打印函数。
代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//提供一个函数,可以打印任意类型的数据
//之所以data使用void *型,是因为可以传入任意类型的地址,要保证兼容性
void myPrint(void* data, void(*mp)(void*)) //使用函数指针作为桥梁
{
mp(data);
}
struct Person
{
char name[64];
int age;
};
void printInt(void* data)
{
int* num = data; //类型转换
printf("%d\n", *num);
}
void printDouble(void* data)
{
double* num = data; //类型转换
printf("%lf\n", *num);
}
void printPerson(void* data)
{
struct Person* num = data; //类型转换
printf("name:%s\tage:%d\n", num->name, num->age);
}
void test01()
{
int a = 10;
myPrint(&a, printInt); //你打印int型数据,需要先自己实现printfInt
double b = 3.5;
myPrint(&b, printDouble); //你打印double型数据,需要先自己实现printfDouble
struct Person p = { "aaa", 18 };
myPrint(&p, printPerson);
}
int main() {
test01();
return EXIT_SUCCESS;
}
printInt、printDouble、printPerson三个函数的参数列表必须一致,因为它们都作为了myPrint函数的参数,既然三个函数的参数要一致,因此它们的参数只能为void *
,即把要打印的数据地址传入,然后在函数内部做类型转换。
上面的程序算比较麻烦的,你都为每种数据类型单独写好了打印函数,还要myPrint干什么?但这里只是为了演示如何使用回调函数,这里是假设函数内部是不知道要打印的数据类型的。
2 打印任意类型的数组
这个比前面的稍微难一点,因为要打印的是数组,假定我们要实现的函数名称为printArray,这里需要考虑以下几个问题:
(1)假如你用空指针做为printArray的形参,那么在函数内部,数据该转换成哪种类型的指针?
(2)考虑到要遍历数据,那么知道数组首元素地址(数组名)后,还得知道步长(其实知道了元素的数据类型后,步长自然就知道了,但我们假设这个函数内部是不知道元素的数据类型的);
(3)如何打印,因为调用printf之前需要知道函数类型;
前两个问题可以一起回答,数组元素的所占字节大小通过参数传入printArray中,作为指针的步长,printArray函数内部,数据转换为char*指针,这样可以和步长配合使用(具体看下面的代码);第三个问题,为每一个类型单独写一个打印函数,然后调用printfArray时,将函数名传入。
代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
//eleSize是元素占用的字节数,使用它可以控制步长
void printAllArray(void* arr, int eleSize, int len, void(*myPrint)(void*))
{
char* p = arr; //之所以用char*转换,是因为char*的步长为1
for (int i = 0; i < len; i++)
{
char* pAddr = p + i * eleSize; //获取每个元素的首地址
myPrint(pAddr); //打印
}
}
void printInt(void* data)
{
int* num = data;
printf("%d\n", *num);
}
struct Person
{
char name[64];
int age;
};
void printPerson(void* data)
{
struct Person* num = data;
printf("name:%s\tage:%d\n", num->name, num->age);
}
void test01()
{
int arr[] = { 1, 2, 3, 4, 5 };
int len = sizeof(arr) / sizeof(arr[0]);
printAllArray(arr, 4, len, printInt);
}
void test02()
{
struct Person personArray[] =
{ {"aaa", 18},
{"bbb", 20},
{"ccc", 30} };
int len = sizeof(personArray) / sizeof(personArray[0]);
printAllArray(personArray, sizeof(struct Person), len, printPerson);
}
int main() {
//test01();
test02();
return EXIT_SUCCESS;
}
3 查找数组中是否存在某个元素
查找的过程其实就是遍历和比较,不同类型的数组元素,比较函数的方式不一样,所以可以把这不一样的部分用函数指针来代替,这里只演示自定义数据类型的比较
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
struct Person
{
char name[64];
int age;
};
int findArrayEle(void* array, int eleSize, int len, void* data, int(*myCompare)(void*, void*))
{
char* p = array;
for (int i = 0; i < len; i++)
{
//获取到每个元素的首地址
char* eleArray = p + eleSize * i;
if (myCompare(eleArray, data))
{
return 1; //查找成功,返回1
}
}
return 0; //查找失败,返回0
}
int comparePerson(void* data1, void* data2)
{
struct Person* p1 = data1;
struct Person* p2 = data2;
if (p1->age == p2->age && strcmp(p1->name, p2->name) == 0)
return 1;
else
return 0;
}
void test03()
{
//查找数组中的元素
struct Person personArray[] =
{ {"aaa", 18},
{"bbb", 20},
{"ccc", 30} };
//数组长度(元素个数)
int len = sizeof(personArray) / sizeof(personArray[0]);
//查找
struct Person p1 = { "ccc", 30 };
int ret = findArrayEle(personArray, sizeof(struct Person), len, &p1, comparePerson);
//打印结果
if (ret)
printf("存在\n");
else
printf("不存在\n");
struct Person p2 = { "ccc", 20 };
ret = findArrayEle(personArray, sizeof(struct Person), len, &p2, comparePerson);
//打印结果
if (ret)
printf("存在\n");
else
printf("不存在\n");
}
int main() {
test03();
return EXIT_SUCCESS;
}
输出:
存在
不存在
4 总结
看完上面3个案例,基本就能理解回调函数了。其实说白了,就是把外层函数中无法用同一个函数实现的环节,用函数指针代替,外层函数根据函数指针的不同,而调用不同的具体函数。
在数据传递的时候(例如上面的传递data),回调函数和函数指针相关的具体函数(如例1中的printInt、printPerson等)最好都使用void*
来传递。回调函数中用char*
去接data,由于char*
的步长为1,因此便于做内存操作;而定义的具体函数(如例1中的printInt、printPerson等),则是进入后首先对数据做类型转换(例1中的printInt函数,内部将data转换成int*
型)。