【问题标题】:What is a "callback" in C and how are they implemented?什么是 C 中的“回调”,它们是如何实现的?
【发布时间】:2010-09-13 15:43:52
【问题描述】:

根据我的阅读,Core Audio 严重依赖回调(和 C++,但这是另一回事)。

我理解设置一个由另一个函数重复调用以完成任务的函数的概念(某种意义上)。我只是不明白它们是如何设置的以及它们是如何工作的。任何示例将不胜感激。

【问题讨论】:

    标签: c callback


    【解决方案1】:

    C 中的回调是提供给另一个函数以在另一个函数执行其任务时“回调”的函数。

    two ways that a callback is used:同步回调和异步回调。向另一个函数提供同步回调,该函数将执行某些任务,然后在任务完成后返回给调用者。向另一个函数提供异步回调,该函数将启动一个任务,然后返回给调用者,该任务可能未完成。

    同步回调通常用于向另一个函数提供委托,另一个函数将任务的某些步骤委托给该函数。此委托的经典示例是 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 操作,然后在完成时调用回调。

    在下面的程序中,我们创建一个套接字来监听 TCP 连接请求,当收到请求时,进行监听的函数会调用提供的回调函数。这个简单的应用程序可以在一个窗口中运行,同时使用telnet 实用程序或网络浏览器尝试在另一个窗口中连接。

    我从 Microsoft 在https://msdn.microsoft.com/en-us/library/windows/desktop/ms737526(v=vs.85).aspx 提供的带有accept() 函数的示例中提取了大部分 WinSock 代码

    此应用程序使用端口 8282 在本地主机 127.0.0.1 上启动 listen(),因此您可以使用 telnet 127.0.0.1 8282http://127.0.0.1:8282/

    此示例应用程序是使用 Visual Studio 2017 社区版作为控制台应用程序创建的,它使用的是 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-*NIX 中使用异步回调的另一个具体示例是异步信号及其信号处理程序。这是对 Linux [链接] (stackoverflow.com/questions/6949025/…) 中如何处理信号处理程序的精彩描述。
    【解决方案2】:

    通过示例更容易理解一个想法。 到目前为止,关于 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);
    }
    

    【讨论】:

      【解决方案3】:

      C 中的回调函数相当于分配给另一个函数使用的函数参数/变量。Wiki Example

      在下面的代码中,

      #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 函数的东西,你可以从它的实例化分配另一个函数在循环内运行。

      【讨论】:

        【解决方案4】:

        一个简单的回调程序。希望它能回答你的问题。

        #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;
        }
        

        【讨论】:

          【解决方案5】:

          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);
              ...
          }
          

          这里,populate_array 函数将函数指针作为它的第三个参数,并调用它来获取用于填充数组的值。我们编写了回调getNextRandomValue,它返回一个随机值,并将指向它的指针传递给populate_arraypopulate_array 将调用我们的回调函数 10 次,并将返回的值分配给给定数组中的元素。

          【讨论】:

          • 我这里可能错了,但是populate_array中调用函数指针的那一行不应该是:array[i] = (*getNextValue)(); ?
          • 解引用运算符对于函数指针是可选的,addressof 运算符也是如此。 myfunc(...) = (*myfunc)(...) 和 &myfunc = myfunc
          • @NathanFellman 我刚刚阅读了Expert C Programming,它很好地解释了函数指针的调用。
          • @johnny 因为标准是这样说的。查看点赞的评论。
          • @Patrick:populateArray 在一个库中(写于 12 年前),而您自己(昨天)编写了 getNextRandomValue;所以它不能直接调用它。想想你自己提供比较器的库排序函数。
          【解决方案6】:

          这里是 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);
          

          【讨论】:

          • 正是我所需要的。如果您的用户想要传递回调函数中所需的自定义数据(例如设备句柄),则 userdata 部分非常有用。
          • 验证题:回调typedef是不是因为是指向函数地址的指针而带星号?如果缺少星号,那会是不正确的吗?如果不正确,那么 github 上 cisco 的 libsrtp 库中缺少两个星号:github.com/cisco/libsrtp/blob/…github.com/cisco/libsrtp/blob/…
          • @twildeman 通过在标准 C 模式下编译并打开警告来回答您自己的问题似乎微不足道。您还可以编写一个最小化的测试程序。诸如libsrtp 中的代码不会给出警告。那么,我假设,当这种类型作为函数参数出现时,它需要“衰减”为指向函数的指针,就像数组衰减为指向其第一个元素的指针一样,所以最后会发生同样的事情无论哪种方式。不过,有趣的是,我发现对此类 typedef 的讨论甚至都没有看这方面,而是专注于声明原型或指针
          • 不知道这是做什么的,也无法编译成功。任何人都可以详细解释它或填写其余代码以成功编译吗?
          【解决方案7】:

          wikipedia article 有一个 C 语言示例。

          一个很好的例子是,为增强 Apache Web 服务器而编写的新模块通过向 Apache 主进程传递函数指针来注册它们,以便回调这些函数以处理网页请求。

          【讨论】:

            【解决方案8】:

            通常这可以通过使用函数指针来完成,这是一个指向函数内存位置的特殊变量。然后,您可以使用它来调用具有特定参数的函数。所以可能会有一个设置回调函数的函数。这将接受一个函数指针,然后将该地址存储在可以使用的地方。之后,当指定的事件被触发时,它将调用该函数。

            【讨论】:

              【解决方案9】:

              C 中的回调通常使用函数指针和关联的数据指针来实现。您将函数on_event() 和数据指针传递给框架函数watch_events()(例如)。当事件发生时,您的函数会使用您的数据和一些特定于事件的数据来调用。

              回调也用于 GUI 编程。 GTK+ tutorialtheory of signals and callbacks 上有一个不错的部分。

              【讨论】:

                猜你喜欢
                • 2020-09-13
                • 2015-04-08
                • 1970-01-01
                • 2021-10-27
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多