一起笨笨的学C——009调试

目录

前言

正文

宏定义

dbg代码

宏中宏排名

实例代码

高级调试技巧

  后语

单元测试

 


前言

        调试宏dbg.h和高级调试技巧!

 


正文

宏定义

        宏字面意思就是广大、博大,用到计算机中,为啥就感觉变怪异了呢?怪异在哪?参考百度词条,宏(计算机术语)_百度百科,解释还算全,为什么呢?它没说二维三维制图里的宏。

        在制图软件中,宏通常指的是一种自定义功能或流程的快捷方式。宏允许用户将一系列的操作和命令记录下来,并将其保存为一个单独的宏文件。当需要执行这些操作时,用户只需调用该宏,而不必逐个手动执行每个操作。这样可以节省时间和精力,提高工作效率。制图软件中的宏通常被用来自动执行复杂的图形操作,例如创建特定类型的图表、应用特定的样式和效果等。宏也可以被用来自定义特定的工作流程,以适应用户的个性化需求。不同的制图软件拥有各自独特的宏系统和宏语法,用户可以根据自身需求和软件的要求进行宏的定义和使用。

        总的来说,宏在计算机里可以理解为强大的工具、宏伟的指令!

        恩,肖老师也是这么总结他的dbg的!那是他多年调试C语言的精华浓缩!

        好了,接着请看调试宏!调试宏是一种在程序中用于辅助调试的宏定义。它们通常在调试阶段使用,用于输出一些调试信息或执行一些特定的调试操作。调试宏可以用于确定程序中的bug或问题,并帮助开发者定位错误所在。

调试宏一般用于以下几个方面:

  1. 打印调试信息:调试宏可以输出一些调试信息,如变量的值、函数的执行路径等,以帮助开发者理解程序的执行过程。

  2. 断言检查:调试宏可以用于在程序中插入断言代码,用于检查某个条件是否满足,如果条件不满足,则触发断言,并输出相应的调试信息。

  3. 计时统计:调试宏可以用于记录程序的执行时间,以便开发者分析和优化程序的性能。

        调试宏一般是通过在代码中定义一些预处理指令来实现的,如使用#define#ifdef等指令。在发布版本中,这些调试宏一般会被禁用或移除,以确保程序的性能和安全性。

dbg代码

#ifndef __dbg_h__
#define __dbg_h__                        //预防不小心包含了两次文件

#include <stdio.h>
#include <errno.h>
#include <string.h>

#ifdef NDEBUG
#define debug(M, ...)                   //不想要调试信息输出,加个”NDEBUG“就好。
#else
#define debug(M, ...) fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#endif                                  //stderr 是一个指向“FILE”对象的指针,该对象与标准错误流相关联,fprintf通俗的讲就是指向file的printf。

#define clean_errno() (errno == 0 ? "None" : strerror(errno))   //errno表示要处理的错误码,strerror返回字符串,表示特定的错误所对应的错误信息。errno = 0 表示没有错误。

#define log_err(M, ...) fprintf(stderr, "[ERROR] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)

#define log_warn(M, ...) fprintf(stderr, "[WARN] (%s:%d: errno: %s) " M "\n", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)

#define log_info(M, ...) fprintf(stderr, "[INFO] (%s:%d) " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)

#define check(A, M, ...) if(!(A)) {log_err(M, ##__VA_ARGS__); errno=0; goto error; }

#define sentinel(M, ...)  { log_err(M, ##__VA_ARGS__); errno=0; goto error; }

#define check_mem(A) check((A), "Out of memory.")

#define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; }

#endif
(END)

        在这个代码中,除了已经注释的,还有一些理解感觉很绕人的(纯个人观点)

    比如M ... ##__VA_ARGS__,还有代码段 fprintf(stderr, "DEBUG %s:%d: " M "\n", __FILE__, __LINE__, ##__VA_ARGS__)。

    M是一个格式化字符串,用于指定要打印的调试信息的格式。(通俗的讲我们指定有异常后,需要打印输出的message。)

    ...是一个可变参数,用于传递给格式化字符串的参数列表。它很神奇,神奇之处在于省略号,省略写很多东西(%d、%s、%f等)。

        宏__VA_ARGS__是一个特殊的宏,在C语言中用于表示可变参数的占位符。它允许将不确定数量的参数传递给一个宏,这些参数可以在宏的定义中使用。在实际使用中,##__VA_ARGS__表示将可变参数展开,并将展开的参数列表作为参数传递给fprintf函数。

        因此,当你使用这个宏时,你可以传递一个格式化字符串和相应的参数,它会将这些信息打印到标准错误流中。例如:

debug("Value of x: %d", x);

 传到fprintf里面,就是 fprintf(stderr, "DEBUG %s:%d: Value of x:%d\n", __FILE__, __LINE__, x) (我理解这个费了很大的劲,小宇宙都爆发好几次了……而且时间长了还容易忘,下次再忘了,我看看此篇博客就好了,NICE!)

将会打印类似于以下内容的调试信息:

DEBUG file.c:10: Value of x: 10

其中file.c是源代码文件的名字,10是该调试信息所在的行号。

宏中宏排名

        为啥我会想到王中王呢?!

        第一check,第二sentinel。

        以上是原创原话,非鄙人意见,大家可以实践感受下。另外,你有没有感觉到宏的无限递归呢?

        

实例代码

#include "dbg.h"
#include <stdlib.h>
#include <stdio.h>
    
void test_debug()
{
    //notice you don't need the \n
    debug("I habe Brown hair");

//passing in arguments like printf
    debug("I am %d years old.", 37);
    }

void test_log_err()
{
    log_err("I believe everything is broken");
    log_err("There are %d problems in %s", 0, "space");
}

void test_log_warn()
{    
    log_warn("You can safely ignore this..");
    log_warn("Maybe consider looking at: %s.",  "/etc/passwd");
}

void test_log_info()
{
    log_info("Well I did something mundane");
    log_info("It happened %f times today.", 1.3f);}


int test_check(char *file_name)
{
    FILE *input = NULL;
    char *block = NULL;

    block = malloc(100);
    check_mem(block);

    input = fopen(file_name, "r");
    check(input, "Failed to open %s..", file_name);

    free(block);
    fclose(input);
    return 0;

error:
    if (block) free(block);
    if (input) fclose(input);
    return -1;
}

int test_sentinel(int code)
{
    char *temp = NULL;//malloc(100*sizeof(char));
    check_mem(temp);

    switch (code) {
        case 1:
            log_info("It worked");
            break;
        default:
            sentinel("I shouldn't run.");
    }
    
    free (temp);
    return 0;
    sentinel("laote 666a")
error:
    if(temp)
        free(temp);
    return -1;
}

int test_check_mem()
{
    char *test = malloc(sizeof(char));
    check_mem(test);

    free(test);
    return 1;

error:
    return -1;
}

int test_check_debug() { int i = 0;
    check_debug(i != 0, "Oops, I was 0.");
    
    return 0;
error:
    return -1;
}

int main (int argc, char *argv[])
{
    check(argc == 2, "Need an argument.");

    test_debug();
    test_log_err();
    test_log_warn();
    test_log_info();
    test_sentinel(1);

    check(test_check("ex19.c") == 0, "failed with ex19.c");
    check(test_check(argv[1]) == -1, "failed with argv");
    check_mem(test_sentinel(1));
//hH    check(test_sentinel(1) == 0, "test_sentinel failed");
    check(test_sentinel(100) == -1, "test_sentinel failed.");
    check(test_check_mem() == -1, "test_check_mem failed");
    check(test_check_debug()== -1, "test_check_debug failed");
    test_check_debug(5);
    return 0;

error:
    return 1;
}

代码输出:

e71993b1ba9b42419452ef26be34d316.png

高级调试技巧

  • 首先用debug()来诊断修复一些逻辑或使用相关问题,九成的错误可以解决(我感觉我知道这个就行了,嘻嘻)
  • 其次用Valgrind捕捉内存错误。(无法解决或者降低程序运行速度用GDB)
  • 最后用GDB解决一些“倔强”。用GDB收集信息,猜到问题后,写一个单元测试重现问题。然后递归此技巧。
  • 调试策略:新建“notes.txt”,写下bug名,假设原因,依次排除原因并记录。(科学的解决问题方法)    

         

    


  后语

  • 细心的你也许发现,我的小标题开始有文字了。其实作为“格式洁癖者”,我不喜欢有文字的。然而我发现如果标题里有些内容的话会节约大家的时间,所以我还是决定加了。
  • dbg.h本以为是我学到的一个秘诀,然而后来通过github发现已经很普及了。不知道你是不是刚看到这个调试宏呢?
  • 高级调试技巧字很少,它其实就是dbg的补充,教你怎么用以及用dbg宏解决不了后该怎么着。
  • 单元测试

        C语言的单元测试是指对程序中最小的可测试单元进行测试的过程,一般以函数为单位进行测试。单元测试的作用主要有以下几点:

  1. 发现和排查错误:单元测试可以帮助发现代码中的错误,包括语法错误、逻辑错误、边界错误等。通过对每个函数进行测试,可以尽早地发现问题并进行修复,减少后续测试和调试的时间和工作量。
  2. 提高代码质量:单元测试可以促使开发者编写高质量的代码。编写单元测试时需要考虑各种情况和边界条件,这有助于开发者更深入地了解自己的代码,并保证代码的正确性和健壮性。
  3. 改善代码设计:编写单元测试需要将代码分解为可以独立测试的模块,这促使开发者进行模块化设计和封装,提高代码的可复用性和可维护性。
  4. 支持重构和优化:单元测试可以保证代码在进行重构或优化时不会引入新的错误。在进行重构和优化之前,运行单元测试可以帮助开发者确认代码的正确性,从而保证重构和优化的安全性和有效性。
  5. 提高开发效率:单元测试可以自动化运行,减少了手工测试的时间和工作量。在进行代码修改、添加新特性等开发过程中,可以通过自动运行单元测试来验证代码的正确性,提高开发效率。
  • 自动测试小技巧

  • 这是我看视频才发现的,天知道我在知道这个方法之前多花了多少时间,索性现在知道了,希望你们能省点时间。
  • 新建一个 test.sh 每次运行 输入sh  test.sh.内容如下:
  • 89a689ac96d5447fa56eb1a71149c70c.png

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值