一.回调函数
在C语言中,回调函数(Callback Function)是一个通过函数指针间接调用的函数。回调函数的实质是将其地址作为一个参数传递给其他函数,这个接收函数会在满足一定条件或需要的时候调用传递过来的函数指针指向的函数。回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。
二.通过转移表进一步引入回调函数
1.转移表:转移表(Jump Table 或 Dispatch Table)是一种数据结构,通常用于优化基于条件分支的执行逻辑。在实现上,转移表常常表现为一个函数指针数组,数组的索引通常是某种形式的枚举值或者字符等,可以根据这些输入值直接查找到对应的函数指针并调用相应的函数,从而取代传统的 switch
或者多个 if-else
分支结构。
2.函数指针数组:
C语言中的函数指针数组是一种特殊的数据结构,它允许存储一组指向不同类型函数的指针。函数指针数组中的每个元素都是一个指向函数的指针,这些函数具有相同的参数列表和返回类型。通过这种方式,程序员可以方便地管理和调用一组相关的函数。
例如,如果我们有一系列做不同数学运算的函数(如加法、减法、乘法和除法),我们可以创建一个函数指针数组,如下所示:
```c
#include <stdio.h>
// 定义各运算函数
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
// 定义函数指针类型,所有函数都有相同的签名
typedef int (*operation_ptr)(int, int);
// 函数指针数组
operation_ptr operations[] = { add, subtract, multiply, divide };
//相当于:int (*operation_ptr[4])(int a,int b)={add,sub,mul,div};
int main() {
int choice, a, b;
printf("Choose an operation (1-4):\n");
scanf("%d", &choice);
// 检查输入的有效性,并确保它在数组范围内
if (choice >= 1 && choice <= 4) {
printf("Enter two numbers: ");
scanf("%d %d", &a, &b);
// 使用函数指针数组调用相应函数
int result = operations[choice - 1](a, b); // 注意:数组下标是从0开始的
printf("Result: %d\n", result);
} else {
printf("Invalid choice!\n");
}
return 0;
}
```
`operations` 是一个函数指针数组,它包含了四个指向同样接受两个整数参数并返回一个整数的函数指针。通过访问数组元素,我们可以间接调用数组中对应的函数。这种方法可以替代繁杂的 `switch` 语句,尤其是在处理大量可选操作的情况下,可以提升代码的组织性和可读性。
3.代码实现转移表:
//回调函数
//函数指针数组
#include <stdio.h>
int Add(int x, int y) {
return x + y;
}
int Sub(int x, int y) {
return x - y;
}
int Mul(int x, int y) {
return x * y;
}
int Div(int x, int y) {
return x / y;
}
//四个函数的返回地址类型相同
//int (*)(int ,int)
void menu() {
printf("****************");
}
void calc(int (*p)(int ,int)) {
//传入的是函数指针:指向相关运算函数
int x = 0;
int y = 0;
int z = 0;
printf("请输入两个操作数:");
scanf_s("%d %d", &x, &y);
z = p(x, y);
printf("%d \n", z);
}
int main() {
int input = 0;
do {
menu();
printf("请选择:");
scanf_s("%d", &input);
switch (input)
{
case 1:
calc(Add);
break;
case 2 :
calc(Sub);
break;
case 3:
calc(Mul);
break;
case 4:
calc(Div);
break;
case 0:
break;
default:
break;
}
} while (input);
return 0;
}
注:
-
使用
switch
语句根据用户的选择调用calc
函数,并将对应的运算函数作为参数传递。比如,当用户选择加法时,调用calc(Add)
,这里的Add
实际上传递的是Add
函数的地址,这就是回调函数的运用——将函数名作为一个值传递给另一个函数去执行。 -
在
calc
函数内部,通过调用传入的函数指针p(x, y)
进行实际的计算操作,实现了动态地根据用户选择调用不同的函数进行计算,而无需在calc
函数内部书写大量重复的分支判断逻辑。 -
简单图解:
三.回调函数的运用:qsort:quick_sort
1.文档解析:MSDN
`qsort` 函数是 C 语言标准库中提供的一个快速排序算法实现,它可以对任何类型的数组进行排序,只需提供一个自定义的比较函数以确定数组元素间的大小关系。
void qsort(void *base, size_t num, size_t width, int (*compare)(const void *, const void *));
- **base**: 指向待排序数组起始位置的指针。
- **num**: 数组中元素的个数。
- **width**: 数组中每个元素所占的字节数。
- **compare**: 用户提供的比较函数,该函数接受两个指向数组元素的指针作为参数,并返回一个整数值来表示这两个元素的大小关系。
- 返回值 `< 0` 表示 `*elem1` 小于 `*elem2`。
- 返回值 `0` 表示 `*elem1` 等于 `*elem2`。
- 返回值 `> 0` 表示 `*elem1` 大于 `*elem2`。
示例中,`main` 函数从命令行参数列表 (`argv`) 中读取参数,并调用 `qsort` 函数对其进行排序。首先排除掉第一个参数(程序名自身),然后通过移动 `argv` 和减少 `argc` 来处理剩下的参数。
自定义的比较函数 `compare` 使用了 `_stricmp` 函数(在 Windows 平台上,这是一个大小写不敏感的字符串比较函数),用于比较两个字符串指针指向的内容。因此,这个程序能够对命令行参数按字母顺序(不区分大小写)进行排序。
运行该程序后,输入的命令行参数会按照指定的排序规则排列,并最终打印出排序后的参数列表。在给出的例子中,输入 "qsort every good boy deserves favor",输出结果为 "boy deserves every favor good",这表明参数已按字母顺序正确排序。
`qsort` 函数无返回值,且兼容 ANSI C 标准和 Windows 95、Windows NT 系统。在微软编译器环境下,该函数存在于 libc.lib(单线程静态库)、libcmt.lib(多线程静态库)和 msvcrt.lib(MSVCRT.dll 导入库)中。同时,`qsort` 函数属于搜索和排序例行程序,相关的还有 `bsearch`(二分查找)和 `_lsearch`(线性查找)等函数。
2.代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <search.h>
int compare(const void *p1,const void *p2) {
return *(int*)p1 - *(int*)p2;
}
int main() {
//qsort排序函数:::quick_sort,适合于任意类型的排序
//void qsort(void* base, size_t num, size_t width, int(__cdecl * compare)(const void* elem1, const void* elem2));
//void* 可以接受任意类型的地址
int nums[] = { 1,634,78,3,7,45,3 };
int len = sizeof(nums) / sizeof(nums[0]);
qsort(nums, len, sizeof(int), compare);
for (int i = 0; i < len; i++) {
printf("%d ", nums[i]);
}
return 0;
}
3.使用qsort排序结构数据:
#include <stdio.h>
#include <stdlib.h>
#include <search.h>
struct Stu
{
char name[20];
int age;
};
int compare(const void* p1, const void* p2) {
return ((struct Stu*)p1)->age - ((struct Stu*)p2)->age;
}
int main() {
struct Stu arr[] = { {"zhan_san",20},{"li_si",50},{"wang_ma_zi",23} };
int len = sizeof(arr) / sizeof(arr[0]);
qsort(arr, len, sizeof(int), compare);
for (int i = 0; i < len; i++) {
printf("%s \n", arr[i]);
}
return 0;
}
四.扩展:
回调函数在python爬虫中的运用
在Python爬虫中,回调函数的应用主要体现在以下几个方面:
1. 异步请求处理:
当爬虫需要并发发起大量HTTP请求时,可以利用异步I/O库如`asyncio`或第三方库`aiohttp`、`requests-futures`等来并发抓取网页。这时,回调函数可以在请求完成时得到调用,处理接收到的响应数据。例如,在`aiohttp`中,可以为异步获取的数据设置回调函数,当HTTP请求完成时,回调函数负责解析和处理响应内容。
```python
async def handle_response(response):
# 解析响应内容
html_content = await response.text()
# 进行后续处理...
async def fetch_url(url, callback):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
await callback(response)
loop = asyncio.get_event_loop()
tasks = [fetch_url(url, handle_response) for url in urls]
loop.run_until_complete(asyncio.wait(tasks))
```
2. 爬虫框架中的中间件或处理器:
在Scrapy这样的高级爬虫框架中,可以通过定义中间件(Middleware)来插入自定义的回调函数。例如,在请求发送前、响应到达后、错误发生时等关键点,都可以注册回调函数来修改请求、处理响应、过滤数据或处理异常情况。
```python
class MyMiddleware:
def process_response(self, request, response, spider):
if response.status == 200:
# 成功获取响应时的回调函数
process_data(response.body)
return response
```
3. 数据处理流程:
在数据抓取完成后,可能需要对抓取到的数据进行清洗、格式化、持久化等操作。这时,可以定义一系列回调函数,分别用来处理不同阶段的任务。例如,当从网页中提取数据之后,可以将提取函数的输出作为参数传递给后续的处理函数。
```python
def extract_data(html):
# 提取数据...
return extracted_data
def process_extracted_data(data):
# 对提取的数据进行预处理...
return processed_data
def save_to_db(processed_data):
# 将处理过的数据保存至数据库...
def scrape_and_process(url):
html = get_html_from_url(url)
data = extract_data(html)
processed_data = process_extracted_data(data)
save_to_db(processed_data)
```
4. 多线程或多进程爬虫中的回调:
在使用线程池或多进程库如`concurrent.futures`进行爬虫开发时,可以将数据解析或处理的函数作为回调函数,当子线程或子进程完成任务时调用。这样可以将耗时较长的网络请求和较快的数据处理解耦,充分利用CPU资源。
```python
def download_and_parse(url):
html = download_page(url)
parsed_data = parse_html(html)
return parsed_data
def handle_parsed_data(data):
# 存储或进一步处理解析后的数据
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_url = {executor.submit(download_and_parse, url): url for url in url_list}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
data = future.result()
handle_parsed_data(data)
except Exception as exc:
print(f'{url} generated an exception: {exc}')
```
总之,回调函数在Python爬虫中扮演着重要的角色,它能帮助开发者在合适的时间点介入数据流,实现高效、灵活的数据抓取和处理流程。
五.总结
回调函数具有以下特点和应用场景:
-
延迟执行:回调函数在特定条件触发后才被执行,而非立即执行,这对于异步编程尤为重要,如网络请求、定时任务等。
-
松耦合:通过回调函数,调用方和被调用方之间解耦,调用方无需了解被调用方的具体实现,只需要提供符合约定的回调函数接口即可。
-
事件驱动:回调函数广泛应用于事件驱动编程中,例如窗口系统、GUI编程、Web前端JavaScript中的事件监听等,当事件触发时,预先定义好的回调函数会被调用。
-
多态性和扩展性:回调函数允许用户自定义行为,增强了程序的多态性和扩展性。例如,在C语言中,通过函数指针传递回调函数;在面向对象语言中,可以通过接口或抽象类实现回调。
-
异步编程:在异步I/O、并发、并行编程中,回调函数常用于处理已完成的任务,如Node.js中的异步API普遍采用回调函数处理异步操作的结果。
-
爬虫技术:在Python爬虫中,回调函数可用于处理异步请求的响应,或在数据处理流程中对抓取到的数据进行实时处理。
-
库函数扩展:很多库函数会提供回调函数接口,允许开发者自定义处理过程,如C标准库中的
qsort
排序函数就需要一个比较函数作为回调。
转载自CSDN-专业IT技术社区
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_56146092/article/details/136366762