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函数的函数指针。这样,对于链表的搜索函数,在实现过程中不需要去具体实现找到节点的方法,只要接收回调函数返回的是否匹配的信息即可,使得搜索函数的适用性更加广泛,实现起来也更加简单。