【Callback接口初探】工程化编程实战Callback接口学习笔记


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

环境:

  • 系统:Manjaro
  • 内核版本:5.4.24
  • gcc 版本:9.2.1
  • VS Code:1.43.0

任务:

  • 在VS Code下编译运行lab5-1.tar.gz
  • 通过VS Code + GDB调试程序找出quit命令无法运行的bug产生的原因
  • 分析Callback接口的运行机制,总结Callback接口设计的方法

编译项目

  1. VS Code下进行编译。

    Terminal>Run Build Task,新建tasks.json文件

  2. 因为要进行链接,所以将其中的args改为如下,其他具体VS Code操作见

    "args": [
        "-g", // 编译的时候需要带-g参数,否则无法进入调试
        "${fileDirname}/linktable.c",   // 这里,在menu.c前,先编译linktable.c
        "${file}",
        "-o",
        "${fileDirname}/${fileBasenameNoExtension}" // 输出文件和源代码同名,不带后缀
    ],
    
  3. 或者直接使用gcc进行编译

    $ gcc linktable.c menu.c -o menu
    

    发现错误menu.c:49:8: 警告:隐式声明函数‘strcmp’这是没有包含进string.h导致的

  4. menu.c文件内加入#include 重新编译

执行项目

  1. 终端输入./menu执行程序

    $ ./menu
    Input a cmd number > help
    help - Menu List:
    help - Menu List:
    version - Menu Program V1.0
    quit - Quit from Menu Program V1.0
    Input a cmd number > quit
    This is a wrong cmd!
    

    发现程序可以运行,但存在bug,无法识别quit命令。

    接下来进入调试,跟踪程序执行情况。

准备调试环境

  1. 首先安装配置好VS Code,打开项目,过菜单Run>Add Configuration,选择C++ (GDB/LLDB)>g++ build and debug active file来创建VS Code调试用的launch.json文件。

    launch.json大致如下:

    {
        // launch.json
        "version": "0.2.0",
        "configurations": [
            {
                "name": "gcc build and debug active file",
                "type": "cppdbg",
                "request": "launch",
                "program": "${fileDirname}/${fileBasenameNoExtension}",
                "args": [],
                "stopAtEntry": false,
                "cwd": "${workspaceFolder}",
                "environment": [],
                "externalConsole": false,
                "MIMode": "gdb",
                "setupCommands": [
                    {
                        "description": "为 gdb 启用整齐打印",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ],
                "preLaunchTask": "gcc build active file",
                "miDebuggerPath": "/usr/bin/gdb"
            }
        ]
    }
    

    配置文件中的stopAtEntry也可以设置为true,这样程序进入调试时,可以自动在main函数设置断点。

调试修复

  1. 回到menu.c文件,在scanf("%s", cmd);后,也就是输入命令后设置断点。

  2. F5进入调试

  3. cmd添加到WATCH

  4. 先输入help,执行

    可以看到,cmd的内容是字符串help,且p指针指向一个tDataNode,通过deschandler可以看出正确匹配命令,所以输出正常

    输入quit后,cmd正常,但指针p指向的是空结点,所以问题可能出在FindCmd函数内(也可能是命令列表初始化时的问题)。

    // FindCmd
    tDataNode* FindCmd(tLinkTable * head, char * cmd)
    {
        return  (tDataNode*)SearchLinkTableNode(head,SearchCondition);
    }
    

    FindCmd函数通过指针函数SearchCondition作为条件,调用linkTable内的SearchLinkTableNode

  5. SearchCondition添加断点

    发现,在进入两次SearchCondition后,就结束了搜索,前两次分别如下

    但linkTable内,在InitMenuData函数中,确实插入了三个结点,调试时也可以看到。

    由只进入两次条件函数,可以断定问题出在SearchLinkTableNode的边界判定上。

    // SearchLinkTableNode
    while(pNode != pLinkTable->pTail)
    {    
        if(Conditon(pNode) == SUCCESS)
        {
            return pNode;				    
        }
        pNode = pNode->pNext;
    }
    

    SearchLinkTableNode的结束条件判断为pNode != pLinkTable->pTail,但pLinkTable->pTail最后一个结点恰好指向命令列表中的最后一个结点quit,所以循环到最后结点时,无法成功进入。

  6. 将循环判断条件改为pNode != NULL程序即可正常运行

callback接口的运行机制

C语言里的Callback机制,靠将函数指针传递到另一函数中,然后在需要触发某个事件的时候调用该方法。这种实现方式简单且灵活,是面向过程的。

在需要回调的函数中先预留一个函数指针参数,接着在外部定义该具体的被回调函数,在执行时将被回调函数作为指针传入,在回调函数中满足某特定条件后,调用该函数。

回调机制能减少不同模块之间的耦合程度,简化了用户接口。

作者:SA19225176,万有引力丶

参考资料来源:USTC SE2020高级软件工程