在C语言中,如何定义用于调试打印的宏?

258

尝试创建一个宏,当 DEBUG 被定义时可以用于打印调试信息,如下伪代码所示:

#define DEBUG 1
#define debug_print(args ...) if (DEBUG) fprintf(stderr, args)

如何使用宏来实现这个?


1
编译器(gcc)是否会优化像if(DEBUG) {...}这样的语句,如果在生产代码中设置DEBUG宏为0?我知道保留调试语句对编译器有好处,但总感觉不太妥当。- Pat - Pat
14个回答

494
如果您使用C99或更高版本的编译器
#define debug_print(fmt, ...) \
            do { if (DEBUG) fprintf(stderr, fmt, __VA_ARGS__); } while (0)

它假定您正在使用C99(早期版本不支持可变参数列表表示法)。 "do {...} while(0)"习惯用法确保代码的行为类似于语句(函数调用)。无条件使用代码确保编译器始终检查您的调试代码是否有效 - 但是当DEBUG为0时,优化器将删除该代码。如果您想使用#ifdef DEBUG一起工作,请更改测试条件:
#ifdef DEBUG
#define DEBUG_TEST 1
#else
#define DEBUG_TEST 0
#endif

在我使用DEBUG的地方,接下来使用DEBUG_TEST。

如果您坚持使用字符串字面量作为格式字符串(这可能是个好主意),您还可以将__FILE____LINE____func__引入输出中,这可以改善诊断:

#define debug_print(fmt, ...) \
        do { if (DEBUG) fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, \
                                __LINE__, __func__, __VA_ARGS__); } while (0)

这依赖于字符串连接来创建比程序员编写的更大的格式化字符串。

如果您使用C89编译器

如果您被困在C89中,没有有用的编译器扩展,那么处理它的方法并不特别干净。我曾经使用的技术是:

#define TRACE(x) do { if (DEBUG) dbg_printf x; } while (0)

然后,在代码中编写:

TRACE(("message %d\n", var));

双括号非常重要,这就是为什么宏展开中有奇怪的符号。与以前一样,编译器始终检查代码的语法有效性(这很好),但只有在DEBUG宏评估为非零时,优化器才调用打印函数。
这确实需要一个支持函数——例如示例中的dbg_printf()——来处理像'stderr'这样的事情。它要求您知道如何编写变量参数函数,但这并不难。
#include <stdarg.h>
#include <stdio.h>

void dbg_printf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
}

你当然也可以在C99中使用这种技术,但是__VA_ARGS__技术更加简洁,因为它使用常规函数符号而不是双括号技巧。
为什么编译器总是看到调试代码非常重要?
[重新阐述了对另一个答案的评论。]
C99和C89实现背后的一个核心思想是,编译器始终看到调试printf样式的语句。这对于长期代码——将持续十年或二十年的代码非常重要。
假设一段代码已经大部分时间处于休眠状态(稳定),但现在需要更改。您重新启用调试跟踪-但是很烦人,因为必须调试调试(跟踪)代码,因为它引用了在稳定维护期间已被重命名或重新输入类型的变量。如果编译器(预处理器后)始终看到打印语句,则确保任何周围的更改都没有使诊断无效。如果编译器未看到打印语句,则不能保护您免受自己的疏忽(或同事或合作者的疏忽)的影响。请参见Kernighan和Pike的“The Practice of Programming”,特别是第8章(另请参见Wikipedia上的TPOP)。
我有过“曾经走过,已经做过”的经验——我使用了其他答案中描述的技术,在非调试版本中多年没有看到类似printf的语句。但是我遇到了TPOP中的建议(请参见我的先前评论),然后在多年后启用了一些调试代码,并遇到了上下文变化破坏调试的问题。很多次,总是验证打印输出已经拯救了我免于后来的问题。
我使用NDEBUG仅控制断言,并使用单独的宏(通常为DEBUG)来控制是否将调试跟踪构建到程序中。即使内置了调试跟踪,我通常也不希望调试输出无条件出现,因此我有机制来控制输出是否出现(调试级别,而不是直接调用fprintf(),我调用一个只有在程序选项基础上有条件地打印的调试打印函数)。我还为更大的程序编写了“多子系统”版本的代码,以便我可以在运行时控制程序的不同部分产生不同量的跟踪。
我主张对于所有版本,编译器都应该看到诊断语句;但是,除非启用了调试,否则编译器不会为调试跟踪语句生成任何代码。基本上,这意味着每次编译时都会由编译器检查所有代码——无论是发布还是调试。这是一件好事!
debug.h - 版本1.2(1990-05-01)
/*
@(#)File:            $RCSfile: debug.h,v $
@(#)Version:         $Revision: 1.2 $
@(#)Last changed:    $Date: 1990/05/01 12:55:39 $
@(#)Purpose:         Definitions for the debugging system
@(#)Author:          J Leffler
*/

#ifndef DEBUG_H
#define DEBUG_H

/* -- Macro Definitions */

#ifdef DEBUG
#define TRACE(x)    db_print x
#else
#define TRACE(x)
#endif /* DEBUG */

/* -- Declarations */

#ifdef DEBUG
extern  int     debug;
#endif

#endif  /* DEBUG_H */

debug.h - 版本 3.6(2008-02-11)

/*
@(#)File:           $RCSfile: debug.h,v $
@(#)Version:        $Revision: 3.6 $
@(#)Last changed:   $Date: 2008/02/11 06:46:37 $
@(#)Purpose:        Definitions for the debugging system
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1990-93,1997-99,2003,2005,2008
@(#)Product:        :PRODUCT:
*/

#ifndef DEBUG_H
#define DEBUG_H

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */

/*
** Usage:  TRACE((level, fmt, ...))
** "level" is the debugging level which must be operational for the output
** to appear. "fmt" is a printf format string. "..." is whatever extra
** arguments fmt requires (possibly nothing).
** The non-debug macro means that the code is validated but never called.
** -- See chapter 8 of 'The Practice of Programming', by Kernighan and Pike.
*/
#ifdef DEBUG
#define TRACE(x)    db_print x
#else
#define TRACE(x)    do { if (0) db_print x; } while (0)
#endif /* DEBUG */

#ifndef lint
#ifdef DEBUG
/* This string can't be made extern - multiple definition in general */
static const char jlss_id_debug_enabled[] = "@(#)*** DEBUG ***";
#endif /* DEBUG */
#ifdef MAIN_PROGRAM
const char jlss_id_debug_h[] = "@(#)$Id: debug.h,v 3.6 2008/02/11 06:46:37 jleffler Exp $";
#endif /* MAIN_PROGRAM */
#endif /* lint */

#include <stdio.h>

extern int      db_getdebug(void);
extern int      db_newindent(void);
extern int      db_oldindent(void);
extern int      db_setdebug(int level);
extern int      db_setindent(int i);
extern void     db_print(int level, const char *fmt,...);
extern void     db_setfilename(const char *fn);
extern void     db_setfileptr(FILE *fp);
extern FILE    *db_getfileptr(void);

/* Semi-private function */
extern const char *db_indent(void);

/**************************************\
** MULTIPLE DEBUGGING SUBSYSTEMS CODE **
\**************************************/

/*
** Usage:  MDTRACE((subsys, level, fmt, ...))
** "subsys" is the debugging system to which this statement belongs.
** The significance of the subsystems is determined by the programmer,
** except that the functions such as db_print refer to subsystem 0.
** "level" is the debugging level which must be operational for the
** output to appear. "fmt" is a printf format string. "..." is
** whatever extra arguments fmt requires (possibly nothing).
** The non-debug macro means that the code is validated but never called.
*/
#ifdef DEBUG
#define MDTRACE(x)  db_mdprint x
#else
#define MDTRACE(x)  do { if (0) db_mdprint x; } while (0)
#endif /* DEBUG */

extern int      db_mdgetdebug(int subsys);
extern int      db_mdparsearg(char *arg);
extern int      db_mdsetdebug(int subsys, int level);
extern void     db_mdprint(int subsys, int level, const char *fmt,...);
extern void     db_mdsubsysnames(char const * const *names);

#endif /* DEBUG_H */

C99或更高版本的单参数变体

Kyle Brandt问道:

Anyway to do this so debug_print still works even if there are no arguments? For example:

    debug_print("Foo");

有一个简单而古老的技巧:

debug_print("%s\n", "Foo");

下面展示的仅适用于GCC的解决方案也提供了支持。
然而,你可以通过使用以下方法在纯C99系统中实现:
#define debug_print(...) \
            do { if (DEBUG) fprintf(stderr, __VA_ARGS__); } while (0)

与第一个版本相比,您失去了需要“fmt”参数的有限检查,这意味着某人可以尝试使用没有参数的方式调用“debug_print()”(但在fprintf()的参数列表中的尾随逗号将无法编译)。 是否丧失检查是一个问题值得商榷。
针对单个参数的GCC特定技术
一些编译器可能提供其他处理宏中可变长度参数列表的方法的扩展。 特别地,在Hugo Ideler的评论中首次提到的,GCC允许您省略通常出现在宏中最后一个“固定”参数后面的逗号。 它还允许您在宏替换文本中使用##__VA_ARGS__,如果前一个标记是逗号,则删除其前面的逗号。
#define debug_print(fmt, ...) \
            do { if (DEBUG) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0)

这种解决方案保留了需要格式参数的好处,同时接受格式后的可选参数。

这种技术也被 Clang 支持,以实现与 GCC 的兼容性。


为什么要使用do-while循环?

这里为什么要使用do while循环?

您希望能够使用宏来模拟函数调用,这意味着它将跟随一个分号。因此,您必须对宏体进行打包以适应。如果您在没有周围的do {...} while (0)的情况下使用if语句,则会有:

/* BAD - BAD - BAD */
#define debug_print(...) \
            if (DEBUG) fprintf(stderr, __VA_ARGS__)

现在,假设你写了:

if (x > y)
    debug_print("x (%d) > y (%d)\n", x, y);
else
    do_something_useful(x, y);

不幸的是,缩进并不能反映实际的流程控制,因为预处理器会生成等效于以下代码的代码(加入缩进和括号以强调实际含义):

if (x > y)
{
    if (DEBUG)
        fprintf(stderr, "x (%d) > y (%d)\n", x, y);
    else
        do_something_useful(x, y);
}

下一个宏的尝试可能是:
/* BAD - BAD - BAD */
#define debug_print(...) \
            if (DEBUG) { fprintf(stderr, __VA_ARGS__); }

现在相同的代码片段会产生:

if (x > y)
    if (DEBUG)
    {
        fprintf(stderr, "x (%d) > y (%d)\n", x, y);
    }
; // Null statement from semi-colon after macro
else
    do_something_useful(x, y);

现在,else已经成为语法错误。使用do { ... } while(0)循环可以避免这两个问题。

还有一种编写宏的方式可能会起作用:

/* BAD - BAD - BAD */
#define debug_print(...) \
            ((void)((DEBUG) ? fprintf(stderr, __VA_ARGS__) : 0))

这将使程序片段保持有效。 (void)强制转换可以防止在需要值的情况下使用它,但它可以用作逗号运算符的左操作数,而do { ... } while(0)版本则不能。 如果您认为应该能够将调试代码嵌入此类表达式中,则可能更喜欢这种方法。 如果您希望要求调试打印作为完整语句执行,则do { ... } while(0)版本更好。 请注意,如果宏的主体涉及任何分号(粗略地说),则只能使用do { ... } while(0)表示法。 它总是有效的; 表达式语句机制可能更难应用。 您还可能会收到编译器的警告,而您希望避免使用表达式形式; 这将取决于您使用的编译器和标志。

TPOP曾经在http://plan9.bell-labs.com/cm/cs/tpophttp://cm.bell-labs.com/cm/cs/tpop上,但现在(2015-08-10)两者都已失效。


GitHub中的代码

如果你感兴趣,可以在我的SOQ(Stack Overflow问题)存储库中查看这些代码,文件位于src/libsoq子目录下的debug.cdebug.hmddebug.c


4
多年过去了,这个回答仍然是互联网上所有关于如何别名 printk 的最有用的回答。在内核空间中,由于没有 stdio,因此无法使用 vfprintf。谢谢! #define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0) - Kevin
9
在关键词__FILE__,__LINE__,__func__,__VA_ARGS__的示例中,如果您没有printf参数,即如果您只调用debug_print(“Some msg\n”);,它将无法编译。通过使用fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__);可以解决这个问题。##__VA_ARGS__允许将无参数传递给函数。 - mc_electron
2
@LogicTom:#define debug_print(fmt, ...)#define debug_print(...)之间的区别在于,前者至少需要一个参数,即格式字符串(fmt)和零个或多个其他参数;后者总共需要零个或多个参数。如果你使用第一个带有debug_print()的宏定义,预处理器会报错,指出宏定义的错误使用方式,而第二个则不会。然而,由于替换文本不是有效的C语言,仍然会导致编译错误。因此,实际上并没有太大的区别,这就是为什么使用术语“有限检查”的原因。 - Jonathan Leffler
1
@JonathanLeffler 很抱歉,我需要更多的解释,您所说的“处理打印本身”是什么意思,为什么要提到flockfile() - HeinrichStack
1
上面展示的变量@St.Antario在整个应用程序中使用单个活动调试级别,我通常使用命令行选项来允许在运行程序时设置调试级别。我还有一个变体,可以识别多个不同的子系统,每个子系统都有一个名称和自己的调试级别,这样我就可以使用“-D input=4,macros=9,rules=2”来将输入系统的调试级别设置为4,将宏系统的调试级别设置为9(正在接受密切审查),将规则系统的调试级别设置为2。主题有无数种变化;使用适合您的任何内容。 - Jonathan Leffler
显示剩余25条评论

32

我使用类似这样的代码:

#ifdef DEBUG
 #define D if(1) 
#else
 #define D if(0) 
#endif

那么我只需要在前面加上D:

D printf("x=%0.3f\n",x);

编译器会看到调试代码,不存在逗号问题,并且它能够在任何地方运行。此外,当printf不足以使用时,例如你必须转储一个数组或计算程序本身多余的一些诊断值时,它也可以使用。

编辑:好的,如果有else,并且被这个插入的if截取了,这可能会产生问题。以下是一个解决方法:

#ifdef DEBUG
 #define D 
#else
 #define D for(;0;)
#endif

3
关于for(;0;),当你写类似于D continue;或者D break;这样的语句时可能会产生问题。 - ACcreator
2
明白了,看起来很不可能是意外发生的。 - mbq
一个更易读的打印版本可能是 #define PRINTF if (0) printf,因为这样你就不需要在每个地方都加前缀了。 - Ed Graham

11

对于一个可移植的(ISO C90)实现,你可以使用双括号,像这样;

#include <stdio.h>
#include <stdarg.h>

#ifndef NDEBUG
#  define debug_print(msg) stderr_printf msg
#else
#  define debug_print(msg) (void)0
#endif

void
stderr_printf(const char *fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  va_end(ap);
}

int
main(int argc, char *argv[])
{
  debug_print(("argv[0] is %s, argc is %d\n", argv[0], argc));
  return 0;
}

或者(拙劣的做法,不建议使用)

#include <stdio.h>

#define _ ,
#ifndef NDEBUG
#  define debug_print(msg) fprintf(stderr, msg)
#else
#  define debug_print(msg) (void)0
#endif

int
main(int argc, char *argv[])
{
  debug_print("argv[0] is %s, argc is %d"_ argv[0] _ argc);
  return 0;
}

3
让预处理器“认为”只有一个参数,同时允许在后期扩展 _。 - Marcin Koziuk

10

以下是我使用的版本:

#ifdef NDEBUG
#define Dprintf(FORMAT, ...) ((void)0)
#define Dputs(MSG) ((void)0)
#else
#define Dprintf(FORMAT, ...) \
    fprintf(stderr, "%s() in %s, line %i: " FORMAT "\n", \
        __func__, __FILE__, __LINE__, __VA_ARGS__)
#define Dputs(MSG) Dprintf("%s", MSG)
#endif

9
我会做类似以下的事情:
#ifdef DEBUG
#define debug_print(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
#else
#define debug_print(fmt, ...) do {} while (0)
#endif

我认为这样更加简洁易懂。

1
@Jonathan:如果代码只在调试模式下执行,那么为什么要关心它是否能在非调试模式下编译?stdlib中的assert()函数也是这样工作的,我通常会重用NDEBUG宏来进行自己的调试代码... - Christoph
在测试中使用DEBUG,如果有人进行了不受控制的undef DEBUG操作,你的代码将无法编译。对吗? - LB40
4
启用调试后,不得不对调试代码进行调试是非常令人沮丧的,因为它可能引用已被重命名或重新类型化的变量等。如果编译器(预处理器之后)始终看到打印语句,则可以确保任何周围的更改未使诊断失效。如果编译器看不到打印语句,则无法保护您免受自己的疏忽(或同事/合作者的疏忽)的影响。请参阅Kernighan和Pike的《编程实践》 - http://plan9.bell-labs.com/cm/cs/tpop/。 - Jonathan Leffler
@Jonathan:这就是为什么你只在发布构建(或测试)中添加-DNDEBUG的原因。 - Christoph
1
@Christoph:嗯,有点像……我使用NDEBUG仅控制断言,并使用单独的宏(通常是DEBUG)来控制调试跟踪。我经常不希望调试输出无条件地出现,因此我有机制来控制输出是否出现(调试级别,而且我不直接调用fprintf(),而是调用一个只有在特定程序选项下才有条件打印的调试打印函数)。我主张对于所有构建版本,编译器都应该看到诊断语句;但是,除非启用了调试,否则它不会生成代码。 - Jonathan Leffler
显示剩余6条评论

9

1
欢迎来到 Stack Overflow。您是正确的,GCC具有您提到的非标准扩展。目前被接受的答案确实提到了这一点,包括您给出的参考URL。 - Jonathan Leffler

8
#define debug_print(FMT, ARGS...) do { \
    if (DEBUG) \
        fprintf(stderr, "%s:%d " FMT "\n", __FUNCTION__, __LINE__, ## ARGS); \
    } while (0)

哪个版本的C支持这种符号表示法?如果它起作用,像那样将所有参数粘合在一起的标记意味着您对格式字符串只有非常有限的选项,是吗? - Jonathan Leffler
@Jonathan:gcc(Debian 4.3.3-13)4.3.3 - eyalm
1
好的 - 我们同意:它被记录为旧的GNU扩展(GCC 4.4.1手册第5.17节)。但是你可能应该记录它只能在GCC中使用 - 或者也许我们在这些评论中已经做到了。 - Jonathan Leffler
1
我的意图是展示另一种使用args的方式,主要是为了演示__FUNCTION__和__LINE__的用法。 - eyalm

2
因此,在使用gcc时,我喜欢:
#define DBGI(expr) ({int g2rE3=expr; fprintf(stderr, "%s:%d:%s(): ""%s->%i\n", __FILE__,  __LINE__, __func__, #expr, g2rE3); g2rE3;})

因为它可以插入到代码中。

假设你正在尝试调试。

printf("%i\n", (1*2*3*4*5*6));

720

然后您可以将其更改为:
printf("%i\n", DBGI(1*2*3*4*5*6));

hello.c:86:main(): 1*2*3*4*5*6->720
720

你可以获得表达式被评估为什么的分析。

它受到双重评估问题的保护,但缺少gensym会导致名称冲突。

然而,它确实可以嵌套:

DBGI(printf("%i\n", DBGI(1*2*3*4*5*6)));

hello.c:86:main(): 1*2*3*4*5*6->720
720
hello.c:86:main(): printf("%i\n", DBGI(1*2*3*4*5*6))->4

我认为只要避免使用g2rE3作为变量名,你就不会有问题。

当然,我发现它(以及针对字符串的类似版本、调试级别等的版本)非常有用。


2

这是我使用的:

#if DBG
#include <stdio.h>
#define DBGPRINT printf
#else
#define DBGPRINT(...) /**/  
#endif

它有一个很好的好处,可以正确处理printf,即使没有其他参数。如果DBG == 0,甚至最愚蠢的编译器也得不到任何可以利用的东西,因此不会生成任何代码。


最好让编译器始终检查调试代码。 - Jonathan Leffler

1

我最喜欢下面的var_dump,当它被调用为:

var_dump("%d", count);

输出如下:

patch.c:150:main(): count = 0

感谢 @"Jonathan Leffler"。所有代码都符合C89标准:

代码

#define DEBUG 1
#include <stdarg.h>
#include <stdio.h>
void debug_vprintf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
}

/* Call as: (DOUBLE PARENTHESES ARE MANDATORY) */
/* var_debug(("outfd = %d, somefailed = %d\n", outfd, somefailed)); */
#define var_debug(x) do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): ", \
    __FILE__,  __LINE__, __func__); debug_vprintf x; }} while (0)

/* var_dump("%s" variable_name); */
#define var_dump(fmt, var) do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): ", \
    __FILE__,  __LINE__, __func__); debug_vprintf ("%s = " fmt, #var, var); }} while (0)

#define DEBUG_HERE do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): HERE\n", \
    __FILE__,  __LINE__, __func__); }} while (0)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接