提问人:noizetoys 提问时间:9/27/2008 最后编辑:Sourav Ghoshnoizetoys 更新时间:7/11/2023 访问量:363965
什么是 C 语言中的“回调”,它们是如何实现的?
What is a "callback" in C and how are they implemented?
问:
从我所做的阅读来看,Core Audio 严重依赖回调(和 C++,但那是另一回事)。
我理解设置一个由另一个函数重复调用以完成任务的函数的概念(某种程度上)。我只是不明白它们是如何设置的以及它们实际上是如何工作的。任何例子将不胜感激。
答:
C 语言中的回调通常使用函数指针和关联的数据指针来实现。例如,将函数和数据指针传递给框架函数。当事件发生时,将使用您的数据和一些特定于事件的数据调用您的函数。on_event()
watch_events()
回调也用于 GUI 编程。GTK+ 教程中有一个很好的部分是关于信号和回调理论的。
通常,这可以通过使用函数指针来完成,函数指针是指向函数内存位置的特殊变量。然后,您可以使用它来调用具有特定参数的函数。所以可能会有一个函数来设置回调函数。这将接受一个函数指针,然后将该地址存储在可以使用它的位置。之后,当触发指定事件时,它将调用该函数。
这篇维基百科文章有一个 C 语言的例子。
一个很好的例子是,为增强 Apache Web 服务器而编写的新模块通过向主 apache 进程传递函数指针来注册它们,以便回调这些函数以处理网页请求。
C 语言中没有“回调”——不比任何其他通用编程概念多。
它们是使用函数指针实现的。下面是一个示例:
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
}
int getNextRandomValue(void)
{
return rand();
}
int main(void)
{
int myarray[10];
populate_array(myarray, 10, getNextRandomValue);
...
}
在这里,该函数将函数指针作为其第三个参数,并调用它来获取要填充数组的值。我们已经编写了回调,它返回一个随机值,并将指向它的指针传递给 。 将调用我们的回调函数 10 次,并将返回的值分配给给定数组中的元素。populate_array
getNextRandomValue
populate_array
populate_array
评论
下面是 C 语言中的回调示例。
假设您要编写一些代码,允许在发生某些事件时调用注册回调。
首先定义用于回调的函数类型:
typedef void (*event_cb_t)(const struct event *evt, void *userdata);
现在,定义一个用于注册回调的函数:
int event_cb_register(event_cb_t cb, void *userdata);
这是注册回调的代码的样子:
static void my_event_cb(const struct event *evt, void *data)
{
/* do stuff and things with the event */
}
...
event_cb_register(my_event_cb, &my_custom_data);
...
在事件调度程序的内部,回调可能存储在如下所示的结构中:
struct event_cb {
event_cb_t cb;
void *data;
};
这是执行回调的代码的样子。
struct event_cb *callback;
...
/* Get the event_cb that you want to execute */
callback->cb(event, callback->data);
评论
libsrtp
一个简单的回调程序。希望它能回答你的问题。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include "../../common_typedef.h"
typedef void (*call_back) (S32, S32);
void test_call_back(S32 a, S32 b)
{
printf("In call back function, a:%d \t b:%d \n", a, b);
}
void call_callback_func(call_back back)
{
S32 a = 5;
S32 b = 7;
back(a, b);
}
S32 main(S32 argc, S8 *argv[])
{
S32 ret = SUCCESS;
call_back back;
back = test_call_back;
call_callback_func(back);
return ret;
}
C 语言中的回调函数等价于分配用于另一个函数的函数参数/变量。Wiki 示例
在下面的代码中,
#include <stdio.h>
#include <stdlib.h>
/* The calling function takes a single callback as a parameter. */
void PrintTwoNumbers(int (*numberSource)(void)) {
printf("%d and %d\n", numberSource(), numberSource());
}
/* A possible callback */
int overNineThousand(void) {
return (rand() % 1000) + 9001;
}
/* Another possible callback. */
int meaningOfLife(void) {
return 42;
}
/* Here we call PrintTwoNumbers() with three different callbacks. */
int main(void) {
PrintTwoNumbers(&rand);
PrintTwoNumbers(&overNineThousand);
PrintTwoNumbers(&meaningOfLife);
return 0;
}
函数调用 PrintTwoNumbers 中的函数 (*numberSource) 是一个函数,用于从 PrintTwoNumbers 内部“回调”/执行,由代码在运行时指示。
因此,如果您有类似 pthread 函数的东西,则可以分配另一个函数从其实例化开始在循环内运行。
C 语言中的回调是提供给另一个函数的函数,当另一个函数正在执行其任务时,该函数在某个时候“回调”。
回调有两种使用方式:同步回调和异步回调。向另一个函数提供同步回调,该函数将执行某些任务,然后在任务完成后返回给调用方。异步回调提供给另一个函数,该函数将启动一个任务,然后返回给调用方,任务可能未完成。
同步回调
同步回调通常用于向另一个函数提供委托,另一个函数将任务的某个步骤委托给该函数。这种委托的典型示例是函数和 C 标准库。这两个函数都接受一个回调,该回调在函数提供的任务期间使用,因此所使用的函数不需要知道正在搜索的数据类型(如果是 ,则为 排序),或者在 的情况下排序的数据类型。bsearch()
qsort()
bsearch()
qsort()
例如,下面是一个使用不同比较函数的小示例程序,演示了同步回调。通过允许我们将数据比较委托给回调函数,该函数允许我们在运行时决定要使用哪种比较。这是同步的,因为当函数返回时,任务已完成。bsearch()
bsearch()
bsearch()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int iValue;
int kValue;
char label[6];
} MyData;
int cmpMyData_iValue (MyData *item1, MyData *item2)
{
if (item1->iValue < item2->iValue) return -1;
if (item1->iValue > item2->iValue) return 1;
return 0;
}
int cmpMyData_kValue (MyData *item1, MyData *item2)
{
if (item1->kValue < item2->kValue) return -1;
if (item1->kValue > item2->kValue) return 1;
return 0;
}
int cmpMyData_label (MyData *item1, MyData *item2)
{
return strcmp (item1->label, item2->label);
}
void bsearch_results (MyData *srch, MyData *found)
{
if (found) {
printf ("found - iValue = %d, kValue = %d, label = %s\n", found->iValue, found->kValue, found->label);
} else {
printf ("item not found, iValue = %d, kValue = %d, label = %s\n", srch->iValue, srch->kValue, srch->label);
}
}
int main ()
{
MyData dataList[256] = {0};
{
int i;
for (i = 0; i < 20; i++) {
dataList[i].iValue = i + 100;
dataList[i].kValue = i + 1000;
sprintf (dataList[i].label, "%2.2d", i + 10);
}
}
// ... some code then we do a search
{
MyData srchItem = { 105, 1018, "13"};
MyData *foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_iValue );
bsearch_results (&srchItem, foundItem);
foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_kValue );
bsearch_results (&srchItem, foundItem);
foundItem = bsearch (&srchItem, dataList, 20, sizeof(MyData), cmpMyData_label );
bsearch_results (&srchItem, foundItem);
}
}
异步回调
异步回调的不同之处在于,当我们向其提供回调的被调用函数返回时,任务可能无法完成。这种类型的回调通常与异步 I/O 一起使用,在异步 I/O 中,启动 I/O 操作,然后在完成后调用回调。
在下面的程序中,我们创建一个套接字来侦听 TCP 连接请求,当收到请求时,执行侦听的函数会调用提供的回调函数。这个简单的应用程序可以通过在一个窗口中运行它来执行,同时使用实用程序或 Web 浏览器尝试在另一个窗口中进行连接。telnet
我从Microsoft提供的 https://msdn.microsoft.com/en-us/library/windows/desktop/ms737526(v=vs.85) 函数的示例中移除了大部分 WinSock 代码.aspxaccept()
此应用程序使用端口 8282 在本地主机 127.0.0.1 上启动 ,因此您可以使用 或 。listen()
telnet 127.0.0.1 8282
http://127.0.0.1:8282/
此示例应用程序是使用 Visual Studio 2017 Community Edition 创建为控制台应用程序的,它使用的是 Microsoft WinSock 版本的套接字。对于 Linux 应用程序,需要将 WinSock 函数替换为 Linux 替代方案,而 Windows 线程库将改用。pthreads
#include <stdio.h>
#include <winsock2.h>
#include <stdlib.h>
#include <string.h>
#include <Windows.h>
// Need to link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")
// function for the thread we are going to start up with _beginthreadex().
// this function/thread will create a listen server waiting for a TCP
// connection request to come into the designated port.
// _stdcall modifier required by _beginthreadex().
int _stdcall ioThread(void (*pOutput)())
{
//----------------------
// Initialize Winsock.
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != NO_ERROR) {
printf("WSAStartup failed with error: %ld\n", iResult);
return 1;
}
//----------------------
// Create a SOCKET for listening for
// incoming connection requests.
SOCKET ListenSocket;
ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET) {
wprintf(L"socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
//----------------------
// The sockaddr_in structure specifies the address family,
// IP address, and port for the socket that is being bound.
struct sockaddr_in service;
service.sin_family = AF_INET;
service.sin_addr.s_addr = inet_addr("127.0.0.1");
service.sin_port = htons(8282);
if (bind(ListenSocket, (SOCKADDR *)& service, sizeof(service)) == SOCKET_ERROR) {
printf("bind failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
//----------------------
// Listen for incoming connection requests.
// on the created socket
if (listen(ListenSocket, 1) == SOCKET_ERROR) {
printf("listen failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
//----------------------
// Create a SOCKET for accepting incoming requests.
SOCKET AcceptSocket;
printf("Waiting for client to connect...\n");
//----------------------
// Accept the connection.
AcceptSocket = accept(ListenSocket, NULL, NULL);
if (AcceptSocket == INVALID_SOCKET) {
printf("accept failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
else
pOutput (); // we have a connection request so do the callback
// No longer need server socket
closesocket(ListenSocket);
WSACleanup();
return 0;
}
// our callback which is invoked whenever a connection is made.
void printOut(void)
{
printf("connection received.\n");
}
#include <process.h>
int main()
{
// start up our listen server and provide a callback
_beginthreadex(NULL, 0, ioThread, printOut, 0, NULL);
// do other things while waiting for a connection. In this case
// just sleep for a while.
Sleep(30000);
}
评论
通过示例来理解一个想法要容易得多。 到目前为止,关于C语言中回调函数的介绍已经是很好的答案,但使用该功能的最大好处可能是保持代码干净整洁。
例
以下 C 代码实现了快速排序。 下面代码中最有趣的一行是这一行,我们可以看到回调函数的运行情况:
qsort(arr,N,sizeof(int),compare_s2b);
compare_s2b 是 qsort() 用来调用该函数的函数的名称。这使 qsort() 保持整洁(因此更易于维护)。你只需从另一个函数内部按名称调用一个函数(当然,至少在从另一个函数调用它之前,函数原型声明必须先于它)。
完整代码
#include <stdio.h>
#include <stdlib.h>
int arr[]={56,90,45,1234,12,3,7,18};
//function prototype declaration
int compare_s2b(const void *a,const void *b);
int compare_b2s(const void *a,const void *b);
//arranges the array number from the smallest to the biggest
int compare_s2b(const void* a, const void* b)
{
const int* p=(const int*)a;
const int* q=(const int*)b;
return *p-*q;
}
//arranges the array number from the biggest to the smallest
int compare_b2s(const void* a, const void* b)
{
const int* p=(const int*)a;
const int* q=(const int*)b;
return *q-*p;
}
int main()
{
printf("Before sorting\n\n");
int N=sizeof(arr)/sizeof(int);
for(int i=0;i<N;i++)
{
printf("%d\t",arr[i]);
}
printf("\n");
qsort(arr,N,sizeof(int),compare_s2b);
printf("\nSorted small to big\n\n");
for(int j=0;j<N;j++)
{
printf("%d\t",arr[j]);
}
qsort(arr,N,sizeof(int),compare_b2s);
printf("\nSorted big to small\n\n");
for(int j=0;j<N;j++)
{
printf("%d\t",arr[j]);
}
exit(0);
}
由于我忘记了动态回调的确切格式,也无法在这里找到它,因此我将留下此评论以备将来参考。 希望它能帮助到其他人。
首先:正如 @aib 等人所解释的那样,C 标准中没有“回调”概念。 它们都只是函数指针,但由于它们通常被称为回调,我也会这样做。
@Richard-Chambers 解释了同步和异步回调。
静态和动态回调
静态回调和动态回调都是异步回调的形式;
- 它们在代码的 1 部分中设置/注册(通过 api 函数)。
- 完整的异步代码段使用回调/函数指针。
静态回调
静态回调的目的是仅将处理程序函数注册到函数指针一次,并在每次发生该事件时调用该函数。 这种类型的回调与 static 关键字无关。
@Russell-Bryant 的例子给出了 atatic 回调的一个很好的例子:首先定义用于回调的函数类型。
!请注意,输入变量/结构不需要是常量,这取决于用例。
typedef void (*type_name)(<function input as normal>);
typedef void (*event_cb_t)(const struct event *evt, void *userdata);
当然,还有(私有/公共)全局函数指针的声明。
!这也可以位于全局(上下文)结构中。
event_cb_t keyboard_input_cb; // might be in another global structure.
// and optional a pointer to user data:
void* my_data;
寄存器功能:
int event_cb_register(event_cb_t cb, void *userdata)
{
//... error checking?
keyboard_input_cb = cb;
my_data = userdata;
}
然后是回调/事件处理程序本身的定义 (my_event_cb) 和对寄存器函数的调用以设置这些指针,如 Russell 的示例所示。
动态回调
动态回调的用途不同:可以从代码中的多个部分更改此指针。 典型的应用程序是网络/协议堆栈,其中每个命令/数据包都会获得对特定处理程序函数的回调。
变体 1:将不同的句柄作为数据结构
当保存回调/指针的句柄/结构与函数的输入结构不同时,代码结构与静态回调大致相同。typedef 是一样的:
typedef void (*event_cb_t)(const struct event *evt, void *userdata);
但指针存储在动态结构中:
typedef struct
{
event_cb_t packet_cb;
uint8_t parameter;
//... other command flags/variables
} packet_handle_t;
还有一些 api 函数可以设置不同的命令/数据包/接口。
int packet_command_raw(packet_handle_t *hpacket)
{
// check if interface/fifo is ready to receive a packet.
//... error checking, command set-up, etc.
memcpy(fifo[i], hpacket, sizeof(hpacket));
// The stack will call the callback inside hpacket when needed.
return NO_ERROR;
}
int packet_command_1(uint8_t byte1, event_cb_t callback)
{
// check if interface/fifo is ready to receive a packet.
//... error checking, command set-up, etc.
fifo[i].parameter = byte1;
fifo[i].packet_cb = callback;
// The stack will call the callback inside hpacket when needed.
return NO_ERROR;
}
变体 2:与数据结构相同的句柄
当保存回调/指针的句柄/结构与函数的输入结构/句柄相同时,指针是自包含的,但需要技巧。
这个技巧是在定义结构内容之前为结构定义一个内部名称。(__packet_handle_t)
当存储在数据包句柄中的事件处理程序也需要数据包句柄本身作为输入时,这是必需的。
typedef struct __packet_handle_t
{
uint8_t parameter;
void (* event1)(struct __packet_handle_t *hpacket);
void (* event2)(struct __packet_handle_t *hpacket);
} packet_handle_t;
在此示例中,在这两种类型的事件上,都调用了不同的处理程序,两者都以句柄作为输入。
当数据包被处理完毕时,指针将丢失(FIFO 条目被清除)。
这种句柄结构也可以在静态回调中找到。
一个典型的用例是外设 HAL 句柄,其中包含一个或多个需要外设句柄作为输入的事件回调。
评论