文章字数:2000 干货指数:⭐⭐⭐⭐⭐
01宏打印函数在我们的嵌入式开发中,使用printf打印一些信息是一种常用的调试手段。但是,在打印的信息量比较多的时候,就比较难知道哪些信息在哪个函数里进行打印。
特别是对于异常情况的打印,我们需要快速定位到异常情况的位置。
这时候我们可以使用宏定义来封装一个宏打印函数,这个宏打印函数可以显示打印信息所在的文件、行数、函数名等信息。如:
#define DBG_PRINTF(fmt, args...) \
{\
printf("<> ", __FILE__, __LINE__, __FUNCTION__);\
printf(fmt, ##args);\
}
左右滑动查看全部代码>>>
使用范例:可见,使用方法与printf的使用方法一样,而且每条打印语句开头都会打印调试信息所在的文件名、行号、函数名信息,方便我们查找一些调试信息。
其中,__FILE__、__LINE__、__FUNCTION__这三个宏是编译器内置宏定义,分别代表调试信息所在文件、行号、函数。
除此之外,常用的宏还有:__DATE__、__TIME__,分别代表当前的编译日期与时间。如:
DBG_PRINTF("Compile Time: %s %s\n", __DATE__, __TIME__);
第二条printf中的##符号是为了处理args不代表任何参数的情况。如:
DBG_PRINTF("Hello world");
当不加##符号是,以上宏的第二条语句被拓展为:
printf("Hello world\n", );
可见,多出了一个逗号,这个逗号是多余的。
加上##符号后,以上宏的第二条语句被拓展为:
printf("Hello world\n");
这才是我们想要的结果。其实这些结果我们通过查看预处理文件可以清晰的知道:
最后需要注意的是,这个DBG_PRINTF还是与printf不一样的。DBG_PRINTF宏是两条语句的组合,无返回值;而printf的原型是:
int printf (const char *__format, ...)
但是我们一般都很少使用printf的返回值,所以DBG_PRINTF的用法与printf函数基本一致。
02打印调试宏开关通常情况下,一些打印调试信息只是在我们调试阶段需要的,在程序发布阶段是不需要的。
所以,为了避免打印调试信息带来的资源开销,我们可以把这些打印调试语句给注释掉。
一种方法是逐句进行注释,这是一种比较低效的方法。比较高效的方法就是添加调试宏开关,利用条件编译来选择打印/不打印调试信息。
比如我们可以把上面的代码改造为:
#define DEBUG 1
#if DEBUG
#define DBG_PRINTF(fmt, args...) \
{\
printf("<> ", __FILE__, __LINE__, __FUNCTION__);\
printf(fmt, ##args);\
}
#else
#define DBG_PRINTF(fmt, args...)
#endif
根据DEBUG宏的值来选择对应的打印宏函数。当DEBUG的值为1时启动相关的打印调试语句,DEBUG的值为0时则关闭打印调试语句。
这样我们就可以很方便的通过设置DEBUG宏的值来启动与关闭我们整个工程的DBG_PRINTF打印调试信息。
03do{}while(0) 其实,上面我们封装的打印宏DBG_PRINTF还有一点缺陷,比如我们与if、else使用的时候,会有这样的一种使用情况:if(c)
{ /* ....... */ };
else
{ /* ....... */ };
显然,出现了语法错误。if之后的大括号之后不能加分号,这里的分号其实可以看做一条空语句,这个空语句会把if与else给分隔开来,导致else不能正确匹配到if,导致语法错误。
为了解决这个问题,有几种方法。第一种方法是:把分号去掉。代码变成:
#define DBG_PRINTF(fmt, args...) \
do\
{\
printf("<> ", __FILE__, __LINE__, __FUNCTION__);\
printf(fmt, ##args);\
}while(0)
这里的do...while循环的循环体只执行一次,与不加循环是效果一样。并且,可以避免了上面的问题。预处理文件:
我们的宏函数实体中,while(0)后面不加分号,在实际调用时补上分号,既符合了C语言语句分号结尾的习惯,也符合了do...while的语法规则。
使用do{}while(0)来封装宏函数可能会让很多初学者看着不习惯,但必须承认的是,这确确实实是一种很常用的方法。
在STM32的HAL库中搜索while(0):
在Linux源码中搜索while(0):
可见,在实际应用中,do{}while(0)用的很多。
04#运算符与##运算符这两个运算符之前也有分享过,这里顺便也提一下。
#号
作为一个预处理运算符
,可以把记号转换成字符串。
例如,如果A是一个宏形参,那么#A就是转换为字符串"A"的形参名。这个过程称为字符串化(stringizing)
。以下程序演示这个过程:
##运算符可以把两个记号组合成一个记号。以下程序演示这个过程:
这个运算符用得很多。如:
《13招!嵌入式 C 语言你不能不知道的优化技巧……》
2《8000字!嵌入式 C 语言你必须知道的数据储存与程序编写知识!》
3《20个嵌入式工程师常见面试题(内附答案)》
![cccecfc47aaf3cae2aff066f6a109efa.png](https://img-blog.csdnimg.cn/img_convert/cccecfc47aaf3cae2aff066f6a109efa.png)
本周直播 | 点击查看?
![d4b4a8f4de1303e16f10646574c421de.png](https://img-blog.csdnimg.cn/img_convert/d4b4a8f4de1303e16f10646574c421de.png)
周一 | 基于STM32物联网项目开发
1、信息采集要如何实现?
2、Zigbee组网开发如何开展?
![60d909ad178d66166f5cb1b80bd81008.png](https://img-blog.csdnimg.cn/img_convert/60d909ad178d66166f5cb1b80bd81008.png)
周二 | 1小时搞定SPI通信原理及应用
1、SPI总线通信原理
2、SPI总线与IIC的异同
3、通信总线应用场景
觉得有用点个在看
![d90319c46f912d08f8731948a0332b06.png](https://img-blog.csdnimg.cn/img_convert/d90319c46f912d08f8731948a0332b06.png)