工程化编程实战callback接口学习笔记
一.VSCode下编译运行
通过如下编译命令
gcc -o test linktable.c menu.c
在当前工作目录下生成一个test.exe可执行文件
二.通过VSCode+GDB调试程序找出quit命令无法运行的bug产生的原因
源码分析
输入quit 发现提示错误命令wrong cmd
试着找出quit无法运行的原因,查看源码中menu.c中发现
tDataNode *p = FindCmd(head, cmd); if( p == NULL) { printf("This is a wrong cmd!\n "); continue; }
在p为null时,输出wrong cmd。接着查看FindCmd函数
/* find a cmd in the linklist and return the datanode pointer */ tDataNode* FindCmd(tLinkTable * head, char * cmd) { return (tDataNode*)SearchLinkTableNode(head,SearchCondition); }
FindCmd函数的返回值又由SearchLinkTableNode的返回值来决定。接着追根溯源查看SearchLinkTableNode函数
#int Conditon(tLinkTableNode * pNode)调用了回调函数
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode)) { if(pLinkTable == NULL || Conditon == NULL) { return NULL; } tLinkTableNode * pNode = pLinkTable->pHead; while(pNode != pLinkTable->pTail) { if(Conditon(pNode) == SUCCESS) { return pNode; } pNode = pNode->pNext; } return NULL; }
这段代码中。引入了一个回调函数的机制:该函数传入的第二个参数其实是一个函数指针,当我们使用这个函数指针所指向的函数时,此时它就被称为回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
通过阅读源码SearchLinkTableNode返回NULL指针的情况:
pNode循环遍历结束以后也未满足Conditon(pNode) == SUCCESS的条件
gdb调试工具分析
PS F:\SSEUSTC\高软\工程化编程实战callback接口学习笔记\lab5.1> gdb -q test Reading symbols from test...done. (gdb) break 49 Breakpoint 1 at 0x401974: file menu.c, line 49. (gdb) run Starting program: F:\SSEUSTC\\callback\lab5.1\test.exe [New Thread 11592.0x2f8c] [New Thread 11592.0x97c] [New Thread 11592.0x1bec] [New Thread 11592.0x2da8] warning: while parsing target library list: not well-formed (invalid token) Input a cmd number > quit Thread 1 hit Breakpoint 1, SearchCondition (pLinkTableNode=0xd81730) at menu.c:49 49 if(strcmp(pNode->cmd, cmd) == 0) (gdb) print pNode->cmd $1 = 0x405009 "help" (gdb) c Continuing. Thread 1 hit Breakpoint 1, SearchCondition (pLinkTableNode=0xd817d0) at menu.c:49 49 if(strcmp(pNode->cmd, cmd) == 0) (gdb) print pNode->cmd $2 = 0x405019 "version" (gdb) n 53 return FAILURE; (gdb) n 54 } (gdb) n SearchLinkTableNode (pLinkTable=0xd816e0, Conditon=0x401960 <SearchCondition>) at linktable.c:151 151 if(Conditon(pNode) == SUCCESS) (gdb) n 155 pNode = pNode->pNext; (gdb) n 149 while(pNode != pLinkTable->pTail) (gdb) print pNode $3 = (tLinkTableNode *) 0xd81820 (gdb) print pLinkTable->pTail $4 = (tLinkTableNode *) 0xd81820 (gdb) n 157 return NULL; (gdb) n 158 } (gdb) n FindCmd (head=0xd816e0, cmd=0x408980 <cmd> "quit") at menu.c:60 60 } (gdb) n main () at menu.c:109 109 if( p == NULL) (gdb) n 111 printf("This is a wrong cmd!\n "); (gdb) n This is a wrong cmd! 112 continue; (gdb)
上述红色部分可以发现:pNode->cmd=“quit”,链表中的尾结点没有参与比较,而且尾结点正是正确的quit命令。
因此将linktable.c文件中的while判断最后一个节点的逻辑改成while(pNode != NULL)
修改后运行结果如下:
三.分析callback接口的运行机制,总结callback接口设计的方法
回调函数定义
让人懵圈的百度百科的解释:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
看上去不是那么容易理解,我们来看个例子(转自知乎):
你到一个商店买东西,刚好你要的东西没货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里:
你的电话号码就叫回调函数,
你把电话留给店员就叫登记回调函数,
店里后来有货了叫做触发了回调关联的事件,
店员给你打电话叫做调用回调函数,
你到店里去取货叫做响应回调事件。
什么时候使用回调函数
在分层设计中,高层次的模块会叫低层次的模块做一些事情,通常是通过函数调用。
从设计上来说,低层次的模块不应该直接调用高层次模块的函数。
所以高层次模块在叫低层模块做事的时候会注册一个回调函数给低层模块,然后低层模块做完了就调用这个函数。表现在C语言上是个函数指针。
本例中即是int Conditon(tLinkTableNode * pNode)
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示(图片来源:维基百科):
回调函数的好处
可以让实现方,根据回调方的多种形态进行不同的处理和操作。
可以让实现方,根据自己的需要定制回调方的不同形态。
可以将耗时的操作隐藏在回调方,不影响实现方其它信息的展示。
让代码的逻辑更加集中,更加易读。