回调函数是C语言中重要的一个知识与应用。在这一篇文章中,你将会彻底理解和使用回调函数。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用的,用于对该事件或条件进行响应。
不出意外,应该是出意外了。你现在看这个定义是不是晕头转向呢?
莫着急,我们一点一点去理解,跟着俺的脚步走嘞!
首先,我们先看一串代码:
void bubble_sort(int arr[], int size) {
int i = 0;
for (i = 0; i < size - 1; i++) {
int j = 0;
for (j = 0; j < size - i-1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int main() {
int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for(i=0;i
上面的代码是通过冒泡排序来对一个整型数组进行排序的过程。但是你是否想过,如果需要排序的不是一个整形数组,而是一个浮点型数组,字符数组,甚至是结构体数组,我们该怎么办呢?
在C语言库函数中有一个函数: qsort( )
它所利用的排序方式是quick sort (在这里我们不深究快排的实现方式)
我们可以通过了解这个函数来认识回调函数。
这个函数的运行机制到底是什么呢?
我们要想了解一个函数,首先要了解函数的返回值和参数:
我们来看一下qsort()的参数和返回值:
void qsort( void *base, size_t num, size_t width, int (__cdecl *compare )(const void *elem1, const void *elem2 ) );
好家伙!这么长?!你确定你没搞我???
别着急啊,我们来一步一步分析:
void qsort( void *base,
size_t num,
size_t width,
int ( *compare )(const void *e1, const void *e2 ) );
首先我们可以把函数简化成如上代码。
我们逐步分析参数:
第一个参数是一个指针类型,
第二个参数和第三个参数描述的是数组的元素个数和数组的宽度也就是数组元素所占字节大小。
第四个参数是一个函数指针。
不出意外,前三个参数你非常的透彻,但是第四个参数晕乎乎是不是呢?
我们现在就来解释一下第四个参数。
我们首先要知道,quick sort 中无论怎么排序,我们都需要比较。所以在qsort中我们就需要传进去一个比较的函数,如果排序整形数组那么就比较整形,字符数组就比较字符等等。而我们也就可以在qsort中使用函数指针来调用cmp函数。
如果你还不理解,没关系,我们可以来剖析下这个函数指针所指的函数,
因为我们传进去的参数的类型必须和我们设计的函数一样,所以我们cmp函数大概如下:
int cmp_int(const void* e1, const void* e2) {
}
显然我们知道这些还不够,我们还要知道e1和e2的意义。
e1和e2代表的是我们要进行比较的两个内容的地址
由于不知道要进行比较的内容的类型,所以接收地址用void*类型。
但是我们在cmp_int中该怎么具体实现呢?
我们查看帮助文档可以看到以下要求:
所以我们现在只需要在函数内部进行e1和e2的比较即可。
但是我们应该如何进行比较呢?是直接对e1和e2进行解引用比较吗?
显然不是的,因为void*类型解引用并不知道解引用的空间大小,所以void*类型是没法进行解引用的。
故,在比较的时候我们应该进行强制类型转换:
int cmp_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
所以我们的cmp_int内部实现应该如上,这时候我们就可以进行两个整形的比较了。
而这时候我们可以调用以下qsort()函数,我们发现调用成功,排序成功了。
int cmp_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
int main() {
int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
//bubble_sort(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for(i=0;i
如果我们现在想排序一个float类型的数组,我们只需要写一个cmp_float,那么这个函数该怎么写呢?
//首先参数和返回类型是固定的
int cmp_float(const void* e1, const void* e2) {
return (int)( * (float*)e1 - *(float*)e2 );
}
void test() {
float arr[10] = { 1.0,3.0,5.0,7.0,9.0,2.0,4.0,6.0,8.0,0.0 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_float);
int i = 0;
for (i = 0; i < sz; i++) {
printf("%f ", arr[i]);
}
}
int main() {
test();
return 0;
}
我们可以看到最后的结果也是可以的。
同样的道理无论是字符型数组,还是结构体数组我们利用qsort函数都可以对其进行排序。
讲述了以上内容后,我们对回调函数有了个模糊的了解,现在我们自己写一个冒泡排序函数,要求可以对任意形式的数据进行排序:
我们设计一个函数,首先我们要清楚函数的返回值和参数:
void bubble_sort(void* base, int size, int width) {
}
我们对一个数组进行排序,我们首先就要知道数组的地址,但是由于我们不清楚数组的类型所以我们接收参数要用void*类型。
我们接收了一个void*类型的指针,但是我们不知道到底传过来的是什么类型,所以我们要通过width来指出传过来的类型大小是多少字节.
而size无需多言,指出我们传过来的数组大小。
ok,写到这里我们可以来实现函数内部的内容了。
void bubble_sort(void* base, int size, int width) {
int i = 0;
//趟数
for (i = 0; i < size; i++) {
//每一趟比较的对数
int j = 0;
for (j = 0; j < size - i - 1; j++) {
//两个元素比较
if () {
}
}
}
}
这时候我们又面对一个问题,我们如何对两个元素进行比较呢?整形我们可以简单的用'>'来比较但是如果是一个字符呢?甚至是一个结构体呢?
无论是对何种数据类型进行比较,比较者是肯定可以清楚比较方式的,所以我们只需要把这个问题交给比较者,而我们只需要设计一个参数来接收这个比较函数即可。
由于我们不清楚比较的两个元素的类型所以我们第四个参数只能按照以下设计方法设计:
void Swap(char* b1, char* b2, int width) {
int i = 0;
for (i = 0; i < width; i++) {
char temp = *b1;
*b1 = *b2;
*b2 = temp;
b1++;
b2++;
}
}
void bubble_sort(void* base, int size, int width,int (*cmp)(void* e1,void* e2)) {
int i = 0;
//趟数
for (i = 0; i < size; i++) {
//每一趟比较的对数
int j = 0;
for (j = 0; j < size - i - 1; j++) {
//两个元素比较
if (cmp((char*)base+j*width,(char*)base+(1+j)*width)>0) {
Swap((char*)base + j * width, (char*)base + (1 + j) * width,width);
}
}
}
}
int cmp_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
int main() {
int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
return 0;
}
以下是对上述代码的解释:
首先在冒泡函数内部比较函数传参时,我们以整形为例,假设需要比较的数组是arr[]={1,3,4,5,6},
那么我们要对相邻的两个元素比较,而我们传递的参数应该是:cmp((char*)base+j*width,(char*)base+(1+j)*width)
原因:我们不清楚比较的数据的类型,所以无法通过传递base和base+1来传参。所以我们可以先把类型强制转换为char*然后我们利用char*一次只访问一个字节内容的特性来传参。
在冒泡排序中我们需要对两个元素进行交换,在交换的函数中我们首先穿进去需要交换的两个元素的地址,而地址为传入cmp的内容,我们只知道地址还不够,因为我们不知道该元素是什么类型,该交换几个字节,所以我们需要把该元素所占字节数width传进去。
代码执行结果如下:
由上述所讲内容,我们可以看到在比较函数中我们未知比较数据的类型,这时候我们把比较的方法抽象成一个函数,而这个函数通过函数指针的方式传递给我们,并且这个函数由使用者来完成。这就是回调函数的意义。