工程化编程实战callback接口学习笔记

 

VSCode配置

  为了使用VSCode调试功能,需要配置launch.json和tasks.json文件,使得VSCode可以编译并启动调试。

  在VSCode中打开lab5.1文件夹,并打开其中的menu.c文件。此时按下Ctrl+Shift+B进行编译,下面的终端会提示编译错误的信息,主要意思是缺少定义。因为menu.c调用了linktable.c中的函数,而默认生成的编译模板只是将自身文件(即menu.c)加入到编译的文件列表中,因此需要修改编译模板即tasks.json文件,将linktable.c手动加入到编译文件列表中。

  点击工具栏上的Terminal,选择Configure Tasks -> C/C++:gcc build active file,VSCode会自动生成适用于gcc编译的tasks.json文件,在第12行原有的内容后面增加"${fileDirname}/linktable.c",,注意不要忘记最后的逗号。最终的tasks.json内容如下:

 1 {
 2     // See https://go.microsoft.com/fwlink/?LinkId=733558 
 3     // for the documentation about the tasks.json format
 4     "version": "2.0.0",
 5     "tasks": [
 6         {
 7             "type": "shell",
 8             "label": "gcc build active file",
 9             "command": "/usr/bin/gcc",
10             "args": [
11                 "-g",
12                 "${file}","${fileDirname}/linktable.c",
13                 "-o",
14                 "${fileDirname}/${fileBasenameNoExtension}"
15             ],
16             "options": {
17                 "cwd": "/usr/bin"
18             },
19             "problemMatcher": [
20                 "$gcc"
21             ],
22             "group": "build"
23         }
24     ]
25 }

  保存后继续进行编译,还有一个warning信息,strcmp函数没有显式声明,这是因为strcmp函数是在string.h头文件中声明的,因此在menu.c中需要包含string.h文件。修改好后编译成功,没有错误和警告信息。

  可以正确编译后,需要配置VSCode的调试功能,这里使用默认的调式模板即可。因此按下F5,选择C++(GDB/LLDB) -> gcc build and debug active file开始调试,VSCode自动生成launch.json文件,并进入调试界面。launch.json的内容如下:

 1 {
 2     // Use IntelliSense to learn about possible attributes.
 3     // Hover to view descriptions of existing attributes.
 4     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
 5     "version": "0.2.0",
 6     "configurations": [
 7         {
 8             "name": "gcc build and debug active file",
 9             "type": "cppdbg",
10             "request": "launch",
11             "program": "${fileDirname}/${fileBasenameNoExtension}",
12             "args": [],
13             "stopAtEntry": false,
14             "cwd": "${workspaceFolder}",
15             "environment": [],
16             "externalConsole": false,
17             "MIMode": "gdb",
18             "setupCommands": [
19                 {
20                     "description": "Enable pretty-printing for gdb",
21                     "text": "-enable-pretty-printing",
22                     "ignoreFailures": true
23                 }
24             ],
25             "preLaunchTask": "gcc build active file",
26             "miDebuggerPath": "/usr/bin/gdb"
27         }
28     ]
29 }

调试程序

  按下F5进入调试界面,先不设置断点运行程序。首先输入"help"显示帮助信息,程序可以正确识别该命令,并输出3个支持的命令,分别为:“help”、"version"和"quit"。  

接着输入"version",看到程序可以正确识别该命令并输出信息。

接着输入"quit",却提示是一个错误的命令,显然程序出错了。

  首先找到输出该错误信息的代码位置,发现在main函数中。程序的main函数内容如下:

 1 int main()
 2 {
 3     InitMenuData(&head); 
 4    /* cmd line begins */
 5     while(1)
 6     {
 7         printf("Input a cmd number > ");
 8         scanf("%s", cmd);
 9         tDataNode *p = FindCmd(head, cmd);
10         if( p == NULL)
11         {
12             printf("This is a wrong cmd!
 ");
13             continue;
14         }
15         printf("%s - %s
", p->cmd, p->desc); 
16         if(p->handler != NULL) 
17         { 
18             p->handler();
19         }
20    
21     }
22 }

  该错误提示在12行,发现当p指针为NULL时会执行该行代码,向上看发现p指针为函数FindCmd的返回值,而FindCmd函数其实是调用的linktable.c中的SearchLinkTableNode函数。函数内容如下:

 1 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))
 2 {
 3     if(pLinkTable == NULL || Conditon == NULL)
 4     {
 5         return NULL;
 6     }
 7     tLinkTableNode * pNode = pLinkTable->pHead;
 8     while(pNode != pLinkTable->pTail)
 9     {    
10         if(Conditon(pNode) == SUCCESS)
11         {
12             return pNode;                    
13         }
14         pNode = pNode->pNext;
15     }
16     return NULL;
17 }

  该函数仅在两种情况下会返回NULL,分别在第5行和第16行,分别对应函数传入的参数错误和while循环中Conditon函数没有SUCCESS两种情况。很显然,由于其他的命令可以正确执行,第一种情况可以排除。因此把调试的精力主要放在第二种情况上。

  因此将调试的断点设在第8行的while循环处(原文件中为149行),打开调试窗口(Ctrl+Shift+D),在终端输入quit命令后程序自动在while循环入口处暂停。如下图所示:

  点击上方调试工具栏的图标或者按下F11进入循环体,继续按下F11进入到Conditon函数,该函数实际上是menu.c中的SearchCondition函数。如下图所示,pNode->cmd值为"help",与需要的"cmd"不同,显然返回的是FAILURE。

  继续进行调试。第二次进入while循环后,当前节点已经指向下一个节点了,也就是"version"节点,同样不是我们需要的值。从Conditon返回后,pNode指针指向链表中的下一个节点,也就是我们需要的"quit'节点。但是在执行while中的条件判断语句时,发现不满足进入循环的条件而直接跳过循环返回NULL了,显然问题出在条件判断语句。

  仔细查看该判断语句,仅当pNode与pLinkTable->pTail不相等时才进入该循环体,而pTail指向的是链表的最后一个节点,也就是说链表的最后一个节点内容不会被遍历到,而quit正好是链表中的最后一个节点,自然也就无法执行其对应的退出操作。因此我们需要修改while函数的判断条件。该处代码的本意是遍历所有的链表节点,因此只需要将判断条件变为pNode != NULL。因为链表的最后一个节点的next指针指向NULL,当遍历完最后一个节点后,pNode变为了NULL,自然无法满足判定条件而退出循环。修改后的代码如下,仅修改了第8行的判断条件:

 1 tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode))
 2 {
 3     if(pLinkTable == NULL || Conditon == NULL)
 4     {
 5         return NULL;
 6     }
 7     tLinkTableNode * pNode = pLinkTable->pHead;
 8     while(pNode != NULL)
 9     {    
10         if(Conditon(pNode) == SUCCESS)
11         {
12             return pNode;                    
13         }
14         pNode = pNode->pNext;
15     }
16     return NULL;
17 }

  重新编译运行,所有的命令都可正常运行,也能识别出错误的命令。

 callback接口的运行机制

 回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

   以上是百度百科中对回调函数的说明,简单来讲,就是程序对外提供一个服务(接口),由用户去决定该服务的具体内容(回调函数)。就比如说航空公司的值机服务,航空公司提供了该服务,并且只需要知道客户是否已经值机,而不需要知道客户是通过何种方式进行值机的(柜台、手机、官网等)。值机服务就类似于callback接口。

  以本次代码中的SearchLinkTableNode函数为例,该函数的其中一个参数Conditon即为一个callback接口。它实际接收的是FindCmd函数传递过来的指向SearchCondition函数的函数指针。这样,对于链表的搜索函数,在实现过程中不需要去具体实现找到节点的方法,只要接收回调函数返回的是否匹配的信息即可,使得搜索函数的适用性更加广泛,实现起来也更加简单。

原文地址:https://www.cnblogs.com/maxiaowei0216/p/12518468.html