单片机/MCU论坛
直播中

松山归人

11年用户 4194经验值
擅长:可编程逻辑,RF/无线
私信 关注
[文章]

【原创文章】程序的调试和宏使用的技巧

作者:蔡琰老师(张飞实战电子高级工程师)
在程序的开发过程中,调试语句是程序开发的一种主要的辅助手段。C语言主要的调试语句使用的是printf,它的定位是系统的标准输出。在嵌入式系统中,printf()的输出可能是屏幕、也可能是串口。


#字符串转化操作符
在编译系统中,可以使用#将当前的内容转换成字符串,例如:
#define dprint(expr)  printf(“
%s = %d n”,#expr,expr)
①在程序中可以使用如下的方式调用:
微信图片_20210818171742.jpg
在以上的例子中,使用#expr表示根据宏中的参数(即表达式的内容),生成一个字符串。因此,#expr代表将dprint(expr)括号中的内容生成一个字符串。一般来说,宏中的参数将作为一个变量被引用,而增加了#修饰之后的表达式,即代表了将宏中的参数名称直接转换成字符串。
上述过程同样是由编译器产生的,编译器在编译源文件的时候,如果遇到了类似的宏(示例中的dprint)会自动根据程序中表达式的内容,生成一个字符串的宏(示例中的#expr)。这样宏同样可以在程序中表示一个字符串。


②进一步,在程序中可以按照如下的形式调用以上宏:
微信图片_20210818171747.jpg
从运行结果的第一行可以看到,编译器的生成字符串的时候,不会照搬宏参数中的所有内容,注释类的内容是不会被放入字符串的宏,这也是因为去注释是编译器预处理阶段的内容,也就是说在实际的编译过程之前,程序中的注释已经被去掉。从运行结果的第二行看出,由于a不是整数,而是字符串的指针,因此打印出a的值实际是变量a的地址,而字符串的内容依然是a。从运行结果的第三行看出,对于直接写入程序的数值(立即数),编译器也可以将它的内容转换成字符串。

这种方式的优点是可以用统一的方法打印表达式的内容,在程序的调试过程中可以方便直观地看到转换成字符串之后的表达式。具体的表达式的内容是什么,是由编译器“自动”写入程序中的,这样使用相同的宏打印所有表达式的字符串。

由于#expr本质就是一个表示字符串的宏,因此在程序中也可以不使用%打印它的内容,而是可以将其直接和其他的字符串连接。上面的宏可以等价为以下的形式:
#define dprint (expr)  printf(“
” #expr “=%d n”,expr)
注意:#是C语言预处理阶段的字符串转化操作符,可以将宏中的内容转换成字符串。


##:连接操作符
在编译系统中,##是C语言中的连接操作符,可以在编译的预处理阶段实现字符串连接的操作。
以下的程序是一个使用##的示例:
#define test(x) test ## x
void test1(int a)      void test2(char *s)
{         {
printf(“Test 1 interger: %d n”,a);   printf(“Test 2 String : %s n”,s);

}         }
微信图片_20210818171759.jpg
在上面这个程序,test(x)宏被定义为test##x,它表示test字符串和x字符串的连接,因此test(1)将被预处理器为:test1,而test(2)将被预处理器处理为:test2。预处理器仅仅是转换字符串而已,所以上面的test1和test2刚好转换成两个函数的名称。

条件编译调试语句
在嵌入式系统的调试中,调试语句可以在程序运行的过程中输出程序的运行状态。然而调试语句的调用是有开销的,在最终发布版的程序中,调试语句都是应该去掉的。去掉调试语句最简单的方式将其注释掉,但是主要就需要维护两种源程序:一种是带有调试语句的调试版程序,另一种是不带有调试语句的发布版程序。这显然不是一种很好的方式,理想的方式是只有一套源程序,根据不同的条件编译选项,编译出不同的调试版和发布版的程序。
在实现的过程中,可以使用一个调试宏来控制调试语句的开关,如下所示:
#ifndef USE_DEBUG
#define DEBUG_OUT(fmt,args...) printf(“File:%s Function:%s Line:%d”fmt,_FILE_,_FUNCTION_,_LINE_,
##args)
#else
#define DEBUG_OUT(fmt,args...)
#endif
在上面的程序中,宏USE_DEBUG用来控制调试语句的开关,当USE_DEBUG被定义的时候,将调试语句DEBUG_OUT定义成上面部分的形式,当没有定义的时候,宏定义为空,在这种情况下,即使程序中写很多DEBUG_OUT,编译器也会将其处理为没有任何语句。
注意:一条语句太长换行需要在每行的结尾使用,表示下一行的内容是和上面的连续的。

使用do...while的宏定义
使用宏定义可以将一些较为短小的功能封装,方便使用。宏的形式和函数类似,但是可以节省函数跳转的开销。如何将一个语句封装成一个宏,在程序中常常使用do...while(0)的形式,例如,对一个简单打印的语句的宏封装如下所示:
#define HELLO(str)  do{ printf(“hello:%sn”,str); }while(0)
在上面这个语句中,将实际执行的功能封装在一个do...while(0)循环内。事实上,do...while(0)由于条件不成立,因此循环体之间的语句只会执行一次。然而,这样做的好处是就是可以让do...while(0)之中的语句像函数一样使用,而不必担心编译器发生错误。
如果直接把后面语句放入宏使用,不用do...while,那么宏在一般顺序执行语句中使用没有问题,如果使用在if...else中,都会发生错误编译。事实上,一般的语句中多一个分号,只相当于多了一条空语句,没有影响。这里却是在if语句后面多出一个分号,它们代表if语句的结束。因此,后面的else就会被视为一条新的语句(相当于前面没有if只有else),这就会发生编译错误。

而如果使用do...while(0)的形式就没有以上的问题,而且一般的C语言编译器都会对do...while(0)进行优化,使其和一般的一条函数等价,在其中可以含有任意条语句。


回帖(1)

王栋春

2021-8-18 21:55:59
非擅长领域,围观为主
举报

更多回帖

发帖
×
20
完善资料,
赚取积分