一、在VSCode下编译运行lab5-1.tar.gz
-
Windows下安装gcc
由于需要在VSCode下编译运行C文件,所以第一步需要在Windows下的VSCode下搭建C开发环境。可以使用mingw-w64来安装gcc环境。方法如下:点击网址下载,下载速度可能过慢,请耐心等待。如实在无法下载成功,可使用分享链接,提取码:enuw。
下载后解压缩,将bin目录添加到环境变量,我的目录为D:\VSCode\mingw64\bin。win+R输入sysdm.cpl回车,在弹出界面选择高级->环境变量。
在系统变量中的Path添加新的环境变量,也就是bin所在绝对路径,之后打开cmd输入gcc -v即可看见gcc版本信息,说明安装成功!
2. 打开VSCode,管理扩展Ctrl+Shift+x,安装C/C++调试器。
至此,VSCode可编译运行并且调试C/C++工程了。
3.编译运行程序
使用如下命令编译程序,生成test可执行文件。有关更多gcc命令可参考:GCC参数详解。
gcc -o test menu.c linktable.c
出现如下报错:
缺少函数strcmp所在的头文件声明,在menu.c中添加string.h头文件,ctrl+s保存,继续编译后报错如下。
缺少pthread.h头文件,解决办法参考:https://blog.csdn.net/CSDN_WHB/article/details/81475233
运行程序./test,分别输入help和quit命令结果如下:
通过运行可以看出,quit命令未能正常运行!
二、通过VSCode+GDB调试程序找出quit命令无法运行的bug产生的原因
1. 代码查看与分析
定位代码,在menu.c文件中main函数如下,其中cmd为一全局变量。
int main() { InitMenuData(&head); /* cmd line begins */ while(1) { printf("Input a cmd number > "); scanf("%s", cmd); tDataNode *p = FindCmd(head, cmd); if( p == NULL) { printf("This is a wrong cmd!\n "); continue; } printf("%s - %s\n", p->cmd, p->desc); if(p->handler != NULL) { p->handler(); } } }
typedef struct LinkTable { tLinkTableNode *pHead; tLinkTableNode *pTail; int SumOfNode; pthread_mutex_t mutex; }tLinkTable;
增加节点函数AddLinkTableNode,可见pHead为链表头(第一个元素),pTail为链表尾(最后一个元素)。
int AddLinkTableNode(tLinkTable *pLinkTable,tLinkTableNode * pNode) { if(pLinkTable == NULL || pNode == NULL) { return FAILURE; } pNode->pNext = NULL; pthread_mutex_lock(&(pLinkTable->mutex)); if(pLinkTable->pHead == NULL) { pLinkTable->pHead = pNode; } if(pLinkTable->pTail == NULL) { pLinkTable->pTail = pNode; } else { pLinkTable->pTail->pNext = pNode; pLinkTable->pTail = pNode; } pLinkTable->SumOfNode += 1 ; pthread_mutex_unlock(&(pLinkTable->mutex)); return SUCCESS; }
其中函数InitMenuData定义了三个命令,help、version和quit,当用户输入相应命令时,会输出对应的命令结果。
int InitMenuData(tLinkTable ** ppLinktable) { *ppLinktable = CreateLinkTable(); tDataNode* pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "help"; pNode->desc = "Menu List:"; pNode->handler = Help; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "version"; pNode->desc = "Menu Program V1.0"; pNode->handler = NULL; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); pNode = (tDataNode*)malloc(sizeof(tDataNode)); pNode->cmd = "quit"; pNode->desc = "Quit from Menu Program V1.0"; pNode->handler = Quit; AddLinkTableNode(*ppLinktable,(tLinkTableNode *)pNode); return 0; }
在main函数中使用了FindCmd函数来判断命令是否合法,当FindCmd函数返回值为空时,输出命令不合法,不为空时输出相应命令。其中FindCmd函数定义如下,可见FindCmd函数返回值为SearchLinkTableNode函数的返回值。
tDataNode* FindCmd(tLinkTable * head, char * cmd) { return (tDataNode*)SearchLinkTableNode(head,SearchCondition); }
此时发现问题了,这个SearchCondition是从哪来的参数呢?按F12发现该参数为一个函数,其定义如下。可见该函数作用为将用户输入的cmd与节点的cmd元素进行比较,若节点中的cmd与用户输入的cmd相同,返回SUCCESS,否则返回FAILURE。
int SearchCondition(tLinkTableNode * pLinkTableNode) { tDataNode * pNode = (tDataNode *)pLinkTableNode; if(strcmp(pNode->cmd, cmd) == 0) { return SUCCESS; } return FAILURE; }
SearchLinkTableNode函数声明如下,可见该函数使用了一个函数指针来作为函数参数。
tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int (*Conditon)(tLinkTableNode * pNode));
然后继续查看SearchLinkTableNode函数定义如下,可知函数指针Condition中的参数pNode由SearchLinkTableNode的第一个参数pLinkTable->pHead得到,也就是说Condition函数指针的参数依赖于SearchLinkTableNode函数中的参数。否则Condition函数指针中的参数无法得到。当然对于一些特殊情况来说,符合条件的全局变量也可作为函数指针Condition的参数而不必完全依赖于SearchLinkTableNode函数。
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; }
由此可知在执行(tDataNode*)SearchLinkTableNode(head,SearchCondition);语句时,该Condition函数指针指向了SearchCondition函数。
从上述分析中可以看出,程序目的是当用户输入help、version和quit会输出相应的命令。但是经过运行发现,help和version命令返回正确,而quit缺返回不到应有的结果(程序退出)。也就是SearchLinkTableNode函数返回了NULL。也就是语句if(Conditon(pNode) == SUCCESS)未能成立,即Condition函数指针指向的SearchCondition函数未能找到cmd值为quit的节点。
通过上述分析可以看出cmd值为quit的节点为链表尾部节点pTail,而在SearchLinkTableNode函数中的while循环中未能遍历到尾部节点pTail导致程序输出错误。这是直观影响,下面通过调试来进行更一步证实。
2. VSCode下的gdb调试
在工作目录下,新建.vscode目录,目录结构如下:
launch.json内容如下:
{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "2,0.0", //配置文件的版本,以前使用是0.2.0,新版本已经弃用,改用为2.0.0 "configurations": [ //配置域 { "preLaunchTask": "Build", //调试会话开始前执行的任务,一般为编译程序。与tasks.json的label相对应 "name": "(gdb) Launch", //配置文件的名字,可以随便起 "type": "cppdbg", //调试的类型,Vscode现在支持很多,我这里主要是C,所以只能是cppdbg "request": "launch",//配置文件的请求类型,有launch和attach两种,具体看官方文档 "targetArchitecture": "x64", //硬件内核架构,为64bit,如图设置 "program": "${fileDirname}/test.exe",//可执行文件的路径和文件名称 "args": ["file1", "file2"],//主函数调用时传入的参数 "stopAtEntry": false,//设为true时程序将暂停在程序入口处 "cwd": "${workspaceFolder}",//调试时的工作目录 "environment": [],//不知道干嘛的 "internalConsoleOptions": "openOnSessionStart",// "externalConsole": true,//调试时是否显示控制台窗口 "MIMode": "gdb",//指定连接的调试器,可以省略不写 "miDebuggerPath": "D:\\VSCode\\mingw64\\bin\\gdb.exe",//调试器路径 "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ], } ] }
tasks.json内容如下:
{ "version": "2.0.0", "tasks": [ { "label": "Build", // 任务名称,与launch.json的preLaunchTask相对应 "command": "gcc", // 要使用的编译器 "args": [ "${fileDirname}/linktable.c", "${fileDirname}/menu.c", "-o", // 指定输出文件名,不加该参数则默认输出a.exe,Linux下默认a.out "${fileDirname}/test.exe",//选择输出的文件名称,和前面的${file}是对应的,一般默认的名称就是前面的${file}.exe "-g", // 生成和调试有关的信息 "-Wall", // 开启额外警告 ], // 编译命令参数 "type": "shell", // 可以为shell或process,前者相当于先打开shell再输入命令,后者是直接运行命令 "group": { "kind": "build", "isDefault": true // 设为false可做到一个tasks.json配置多个编译指令,需要自己修改本文件 }, "presentation": { "echo": true, "reveal": "always", // 在“终端”中显示编译信息的策略,可以为always,silent,never。具体参见VSC的文档 "focus": false, // 设为true后可以使执行task时焦点聚集在终端 "panel": "shared" // 不同的文件的编译信息共享一个终端面板 }, } ] }
settings.json内容如下:
{ "files.associations": { "stdio.h": "c", } }
文件内容设置正确之后,就能够进行VSCode下的gdb调试了。
断点设置如下:
按F5开始调试,输入quit后,程序在断点处停,常见调试命令如下:
根据如上的调试命令进行调试,按F11直到进入SearchLinkTableNode函数内部,可以看出链表pLinkTable共有三个节点,且函数指针Condition指向了SearchCondition函数。第一个节点所在地址为0x848d0,最后一个节点pTail所在地址为0x849c0,并且最后一个节点保存了quit命令。
进一步调试进入SearchCondition函数内部,按F11跳入了SearchCondition函数,进一步证实了Condition函数指针指向了SearchCondition函数。
第一次进入SearchCondition函数时,调试结果如下。可见第一个节点的cmd为help,与输入的cmd(quit)不等,返回FAILURE。
第二次进入SearchCondition函数时,调试结果如下。可见第二个节点的cmd为version,与输入的cmd(quit)不等,返回FAILURE。
之后再次进入SearchLinkTableNode函数的while循环的判断语句时,当前pNode和pLinkTable->pTail地址值相同,退出while循环。由此可以看出SearchLinkTableNode函数并没有对尾节点也就是quit命令所在的节点进行判断,所以也就导致了quit命令未能正常输出。
3. 代码修改
调试发现的确是未能正确遍历所有节点,所以可以将SearchLinkTableNode函数修改如下:
/*修改后的SearchLinkTableNode*/ tLinkTableNode * SearchLinkTableNode(tLinkTable *pLinkTable, int Conditon(tLinkTableNode * pNode)) { if(pLinkTable == NULL || Conditon == NULL) { return NULL; } tLinkTableNode * pNode = pLinkTable->pHead; while(pNode != NULL) //遍历所有节点 { if(Conditon(pNode) == SUCCESS) { return pNode; } pNode = pNode->pNext; } return NULL; }
编译运行结果如下,符合预期。
三、分析callback接口的运行机制,总结callback接口设计的方法
-
运行机制
回调函数:在计算机程序设计中,回调函数,或简称回调,是指通过函数参数传递到其它代码的,某一块可执行代码的引用。这一设计允许了底层代码调用在高层定义的子程序。通过对上述程序的分析,我们可以发现当函数A里有一个函数指针B作为参数时,函数指针B可以指向与函数指针里的参数相符合的任意函数,也就是说我们可以通过改变函数指针B不同指向来实现不同的功能,这就相当于对不同的函数进行了一层封装。这就和C++中的通过一个基类可以有多个子类,而子类有有独属于自己的特性和一个类有不同的对象,每个对象有不同特征有异曲同工之妙。这就使函数变得抽象起来,这可能与我们所熟知的C语言是面向过程的特点不同,有点像C++中的面向对象。这也增加了可重用性,提高了软件产品的可复用能力,更具有通用性。
上图很好的展现了回调函数的运行机制,主程序只需要调用库函数,而库函数通过不同的Callback function的调用可实现多种不同功能。对于来说比较熟悉的就是STL的排序的排序了,如果要对自定义的数据类型进行排序,在排序规则不明的情况下,这个时候就需要自己定义一个比较大小的回调函数,里面定义一个比较规则。然后将函数指针传入STL的排序函数中。这样STL在比较两个元素大小的时候,就会调用自定义比较函数了,而不会不知道怎么排序。
又例如我们在main.c中添加如下函数:
int AllTrueCondition(tLinkTableNode * pLinkTableNode) { return SUCCESS; }
将FindCmd函数修改如下:
tDataNode* FindCmd(tLinkTable * head, char * cmd) { return (tDataNode*)SearchLinkTableNode(head,AllTrueCondition); }
重新编译运行结果如下:
当我们输入任何值时,输出都会变为输出所有命令。从这就可以看出,当我们的软件需要添加新功能时,不必修改底层代码,只需要写出与功能相符合的相应接口就可以完成。这对于代码的维护起到了很好的作用。
2. Callback接口设计方法
有些同学可能会发现了,SearchLinkTableNode函数的定义为什么不可用定义为如下,即只用函数指针作为参数呢?
/*错误的定义方式*/ tLinkTableNode * SearchLinkTableNode(int (*Conditon)(tLinkTableNode * pNode));
这里就有问题了,函数指针Conditon中的参数pNode要从哪获得呢?这显然是不合理的。所以在用函数指针作为参数时,一定要注意函数指针里的参数要如何获得,需要的参数又是哪些,要设计出符合功能要求而又不至于过于繁琐的函数指针。
所以接口方法总结如下:
- 根据所需功能定义一个合适的回调函数
- 将回调函数作为参数的函数也就是调用函数要注意回调函数中的参数来进行相应的设计,以满足回调函数参数调用需求
- 将符合要求的参数以及函数指针作为调用函数的参数。当用户调用时,只需要将函数指针指向不同的函数即可