周立功教你学程序设计技术:做好软件模块的分层设计,回调函数要这样写
直接调用下层提供的函数,但下层不能直接调用上层提供的函数,且层与层之间绝对不能循环调用。因为层与层之间的循环依赖会严重妨碍软件的复用性和可扩展性,使得系统中的每一层都无法独立构成一个可复用的组件。虽然上层也可以调用相邻下层提供的函数,但不能跨层调用。即下层模块实现了在上层模块中声明并被高层模块调用的接口,这就是著名的好莱坞(Hollywood)扩展原则:"不要调用我,让我调用你。"当下层需要传递数据给上层时,则采用回调函数指针接口隔离变化。通过倒置依赖的接口所有权,创建了一个更灵活、更持久和更易于修改的结构。
实际上,由上层模块(即调用者)提供的回调函数的表现形式就是在下层模块中通过函数指针调用另一个函数,即将回调函数的地址作为实参初始化下层模块的形参,由下层模块在某个时刻调用这个函数,这个函数就是回调函数,详见图 2.3。其调用方式有两种:
在上层模块A调用下层模块B的函数中,直接调用回调函数C;
使用注册的方式,当某个事件发生时,下层模块调用回调函数。
图 2.3 回调函数的使用
在初始化时,上层模块A将回调函数C的地址作为实参传递给下层模块B。在运行中,当下层模块需要与上层模块通信时,调用这个回调函数。其调用方式为A→B→C,上层模块A调用下层模块B,在B的执行过程中,调用回调函数将信息返回给上层模块。对于上层模块来说,C不仅监视B的运行状态,而且干预B的运行,其本质上依然是上层模块调用下层模块。由于增加了回调函数,即可在运行中实现动态绑定,下面将以标准的冒泡排序函数对一个任意类型的数据进行排序为例予以说明。
(2)数据比较函数
假设待排序的数据为int型,即可通过比较相邻数据的大小,做出是否交换数据的处理。当给定两个指向int型变量的指针e1和e2时,则比较函数返回一个数。如果*e1小于*e2,那么返回的数为负数;如果*e1大于*e2,那么返回的数为正数;如果*e1等于*e2,那么返回的数为0,详见程序清单 2.4。
程序清单 2.4 compare_int()数据比较函数
1 int compare_int(const int *e1, const int *e2)
2 {
3 return *e1 - *e2; // 升序比较
4 }
5
6 int compare_int(const int *e1, const int *e2)
7 {
8 return *e2 - *e1; // 降序比较
9 }
由于任何数据类型的指针都可以给void*指针赋值,因此可以利用这一特性,将void*指针作为数据比较函数的形参。当函数的形参声明为void *类型时,虽然bubbleSort()冒泡排序函数内部不知道调用者会传递什么类型的数据过来,但调用者知道数据的类型和对数据的操作方法,那就由调用者编写数据比较函数。
由于在运行时调用者要根据实际情况才能决定调用哪个数据比较函数,因此根据比较操作的要求,其函数原型如下:
typedef int (*COMPARE)(const void *e1, const void *e2);
其中的e1、e2是指向2个需要进行比较的值的指针。当返回值< 0时,表示e1 < e2;当返回值= 0时,表示e1 = e2;当返回值> 0时,表示e1 > e2。
当用typedef声明后,COMPARE就成了函数指针类型,有了类型就可以定义该类型的函数指针变量。比如:
COMPARE compare;
此时,只要将函数名(比如,compare_int)作为实参初始化函数的形参,即可调用相应的数据比较函数。比如:
COMPARE compare=compare_int;
虽然编译器看到的是一个compare,但调用者实现了多种不同类型的compare,即可根据接口函数中的类型改变函数的行为方式,通用数据比较函数的实现详见程序
- 电源软启动的实用设计技巧(07-16)
- 周立功:动态分布内存——malloc()函数与calloc()函数(07-22)
- 周立功“程序设计与数据结构”:深度解剖动态分布内存的free()函数与realloc()函数(07-25)
- 周立功教你学C语言编程:教你数组是如何保存指针的(07-31)
- 算法的泛化问题,这些坑你可能都经历过!|周立功教你学软件设计(08-01)
- 所有C语言数组和指针的知识都在这里了!|周立功手把手教你学C语言编程(08-01)