目录
前言
调试宏dbg.h和高级调试技巧!
正文
宏定义
宏字面意思就是广大、博大,用到计算机中,为啥就感觉变怪异了呢?怪异在哪?参考百度词条,宏(计算机术语)_百度百科,解释还算全,为什么呢?它没说二维三维制图里的宏。
在制图软件中,宏通常指的是一种自定义功能或流程的快捷方式。宏允许用户将一系列的操作和命令记录下来,并将其保存为一个单独的宏文件。当需要执行这些操作时,用户只需调用该宏,而不必逐个手动执行每个操作。这样可以节省时间和精力,提高工作效率。制图软件中的宏通常被用来自动执行复杂的图形操作,例如创建特定类型的图表、应用特定的样式和效果等。宏也可以被用来自定义特定的工作流程,以适应用户的个性化需求。不同的制图软件拥有各自独特的宏系统和宏语法,用户可以根据自身需求和软件的要求进行宏的定义和使用。
总的来说,宏在计算机里可以理解为强大的工具、宏伟的指令!
恩,肖老师也是这么总结他的dbg的!那是他多年调试C语言的精华浓缩!
好了,接着请看调试宏!调试宏是一种在程序中用于辅助调试的宏定义。它们通常在调试阶段使用,用于输出一些调试信息或执行一些特定的调试操作。调试宏可以用于确定程序中的bug或问题,并帮助开发者定位错误所在。
调试宏一般用于以下几个方面:
-
打印调试信息:调试宏可以输出一些调试信息,如变量的值、函数的执行路径等,以帮助开发者理解程序的执行过程。
-
断言检查:调试宏可以用于在程序中插入断言代码,用于检查某个条件是否满足,如果条件不满足,则触发断言,并输出相应的调试信息。
-
计时统计:调试宏可以用于记录程序的执行时间,以便开发者分析和优化程序的性能。
调试宏一般是通过在代码中定义一些预处理指令来实现的,如使用#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;
}
代码输出:
高级调试技巧
- 首先用debug()来诊断修复一些逻辑或使用相关问题,九成的错误可以解决(我感觉我知道这个就行了,嘻嘻)
- 其次用Valgrind捕捉内存错误。(无法解决或者降低程序运行速度用GDB)
- 最后用GDB解决一些“倔强”。用GDB收集信息,猜到问题后,写一个单元测试重现问题。然后递归此技巧。
- 调试策略:新建“notes.txt”,写下bug名,假设原因,依次排除原因并记录。(科学的解决问题方法)
后语
- 细心的你也许发现,我的小标题开始有文字了。其实作为“格式洁癖者”,我不喜欢有文字的。然而我发现如果标题里有些内容的话会节约大家的时间,所以我还是决定加了。
- dbg.h本以为是我学到的一个秘诀,然而后来通过github发现已经很普及了。不知道你是不是刚看到这个调试宏呢?
- 高级调试技巧字很少,它其实就是dbg的补充,教你怎么用以及用dbg宏解决不了后该怎么着。
-
单元测试
C语言的单元测试是指对程序中最小的可测试单元进行测试的过程,一般以函数为单位进行测试。单元测试的作用主要有以下几点:
- 发现和排查错误:单元测试可以帮助发现代码中的错误,包括语法错误、逻辑错误、边界错误等。通过对每个函数进行测试,可以尽早地发现问题并进行修复,减少后续测试和调试的时间和工作量。
- 提高代码质量:单元测试可以促使开发者编写高质量的代码。编写单元测试时需要考虑各种情况和边界条件,这有助于开发者更深入地了解自己的代码,并保证代码的正确性和健壮性。
- 改善代码设计:编写单元测试需要将代码分解为可以独立测试的模块,这促使开发者进行模块化设计和封装,提高代码的可复用性和可维护性。
- 支持重构和优化:单元测试可以保证代码在进行重构或优化时不会引入新的错误。在进行重构和优化之前,运行单元测试可以帮助开发者确认代码的正确性,从而保证重构和优化的安全性和有效性。
- 提高开发效率:单元测试可以自动化运行,减少了手工测试的时间和工作量。在进行代码修改、添加新特性等开发过程中,可以通过自动运行单元测试来验证代码的正确性,提高开发效率。
-
自动测试小技巧
- 这是我看视频才发现的,天知道我在知道这个方法之前多花了多少时间,索性现在知道了,希望你们能省点时间。
- 新建一个 test.sh 每次运行 输入sh test.sh.内容如下: