#define宏,用于在C中进行调试打印?


209

尝试创建定义DEBUG时可用于打印调试消息的宏,例如以下伪代码:

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

如何使用宏完成此操作?


如果在生产代码中将DEBUG宏设置为0,则编译器(gcc)是否会优化if(DEBUG){...}之类的语句?我知道有充分的理由使调试语句对编译器可见,但是仍然有一种不好的感觉。-Pat
Pat

Answers:


410

如果使用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”之类的事情。它要求您知道如何编写varargs函数,但这并不难:

#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的语句。这对于长期的代码很重要,这种代码将持续十年或两年。

假设一段代码大多数年来一直处于休眠状态(稳定),但是现在需要进行更改。您可以重新启用调试跟踪-但是必须调试调试(跟踪)代码令人沮丧,因为在稳定维护的几年中,它引用了已重命名或重新键入的变量。如果编译器(后预处理器)始终看到print语句,则可以确保所有周围的更改都不会使诊断无效。如果编译器看不到打印语句,则它无法保护您免受自己的粗心(或同事或合作者的粗心)的伤害。请参阅Kernighan和Pike 的“编程实践 ”,尤其是第8章(另请参见TPOP上的Wikipedia)。

这是“到那里,就那样做”的经验-我本质上使用了其他答案中描述的技术,其中非调试版本多年来(超过十年)都看不到类似于printf的语句。但是我遇到了TPOP中的建议(请参阅我以前的评论),然后在几年后确实启用了一些调试代码,并且遇到了上下文更改中断调试的问题。好几次,始终对打印进行验证使我免于以后的麻烦。

我使用NDEBUG仅控制断言,并使用单独的宏(通常是DEBUG)来控制是否在程序中内置了调试跟踪。即使内置了调试跟踪,我也经常不希望调试输出无条件显示,因此我有一种机制来控制输出是否出现(调试级别,而不是fprintf()直接调用,而是调用仅有条件打印的调试打印功能)因此根据程序选项可以打印或不打印相同版本的代码)。对于大型程序,我还有一个代码的“多个子系统”版本,因此我可以在运行时控制下,使程序的不同部分产生不同数量的跟踪。

我主张对于所有构建,编译器都应查看诊断语句。但是,除非启用了debug,否则编译器不会为调试跟踪语句生成任何代码。基本上,这意味着您每次编译时都会检查编译器的所有代码-是发布还是调试。这是一件好事!

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)问:

无论如何,debug_print即使没有参数,它仍然可以工作吗?例如:

    debug_print("Foo");

有一个简单的老式hack:

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)

该解决方案保留了要求format参数的优点,同时在格式之后接受可选参数。

Clang也为GCC兼容性支持此技术。


为什么执行do-while循环?

这里的目的是do while什么?

您希望能够使用该宏,使其看起来像一个函数调用,这意味着后面将使用分号。因此,您必须包装宏主体以适合。如果您使用的if语句周围没有do { ... } while (0),则将具有:

/* 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中的代码

如果您好奇,可以在GitHub的SOQ(堆栈溢出问题)存储库中以文件形式查看此代码debug.cdebug.hmddebug.csrc / libsoq 子目录中查看


1
我认为在“单一参数C99变体”标题下应该提及来自gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html的GCC ##方法。
雨果·伊德勒

2
多年后,在如何给printk加上别名的情况下,这个答案仍然是所有互联网中最有用的!vfprintf在内核空间中不起作用,因为stdio不可用。谢谢! #define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0)
凯文夫

6
在带有关键字的示例中,__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 2014年

1
@LogicTom:#define debug_print(fmt, ...)和之间的区别#define debug_print(...)。其中第一个需要至少一个参数,格式字符串(fmt)和零个或多个其他参数;第二个总共需要零个或多个参数。如果debug_print()与第一个一起使用,则会从预处理器收到有关滥用宏的错误,而第二个则不会。但是,由于替换文本不是有效的C,您仍然会遇到编译错误。因此,它的确没有太大区别-因此使用了术语“有限检查”。
乔纳森·勒夫勒

1
上面显示的变体@ St.Antario在整个应用程序中使用单个活动的调试级别,并且我通常使用命令行选项来允许在程序运行时设置调试级别。我还有一个变体,它可以识别多个不同的子系统,每个子系统都有一个名称和自己的调试级别,以便可以-D input=4,macros=9,rules=2将输入系统的调试级别设置为4,将宏系统的调试级别设置为9(经过严格的审查) )和2的规则系统。主题有无尽的变化;使用适合您的任何东西。
乔纳森·莱夫勒19'Apr

28

我用这样的东西:

#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 2014年

1
有我 不过,它似乎不太可能偶然发生。
mbq 2015年

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
@LB:使预处理器“认为”只有一个参数,而让_在以后的阶段扩展。
Marcin Koziuk,09年

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

我认为这比较干净。


我真的不喜欢在测试中使用宏作为标志的想法。您能否解释为什么应始终检查调试打印?
2009年

1
@Jonathan:如果代码只在调试模式下执行,为什么还要在非调试模式下编译呢?assert()来自stdlib的工作方式相同,我通常只是将NDEBUG宏重新用于自己的调试代码...
Christoph

在测试中使用DEBUG,如果有人执行不受控制的undef DEBUG,则您的代码将不再编译。对 ?
LB40

4
启用调试然后必须调试调试代码令人沮丧,因为它引用已重命名或重新键入的变量,等等。如果编译器(后预处理器)始终看到print语句,则可确保进行任何周围的更改没有使诊断无效。如果编译器看不到打印语句,则它无法保护您免受自己的粗心(或同事或合作者的粗心)的伤害。请参阅Kernighan和Pike 撰写的“编程实践” -plan9.bell-labs.com/cm/cs/tpop
乔纳森·莱夫勒

1
@Christoph:好吧,...我使用NDEBUG仅控制断言,并使用单独的宏(通常为DEBUG)来控制调试跟踪。我经常不希望调试输出无条件显示,因此我有一种机制来控制输出是否显示(调试级别,而不是直接调用fprintf(),而是调用一个调试打印函数,该函数仅有条件地进行打印,因此生成代码可以根据程序选项打印或不打印)。我主张对于所有构建,编译器都应查看诊断语句。但是,除非启用调试,否则它不会生成代码。
乔纳森·勒夫勒

8

根据http://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html,应该有一个##before __VA_ARGS__

否则,宏#define dbg_print(format, ...) printf(format, __VA_ARGS__)不会编译下面的例子:dbg_print("hello world");


1
欢迎使用堆栈溢出。您正确地认为GCC具有您引用的非标准扩展名。实际上,当前接受的答案确实提到了这一点,包括您提供的参考URL。
乔纳森·莱夫勒

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

哪个C版本支持该表示法?而且,如果可行,令牌会粘贴所有这样的参数,这意味着格式字符串的选项集非常有限,不是吗?
乔纳森·莱夫勒

@Jonathan:gcc(Debian 4.3.3-13)4.3.3
eyalm

1
好的-同意:它被记录为旧的GNU扩展(GCC 4.4.1手册的5.17节)。但是您可能应该证明它只能与GCC一起使用-或者也许我们已经在这些评论中做到了这一点。
乔纳森·莱夫勒

1
我的意图是展示使用args的另一种风格,主要是演示FUNCTIONLINE
eyalm

2

这是我用的:

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

即使没有其他参数,它也具有正确处理printf的好处。在DBG == 0的情况下,即使最笨拙的编译器也没有任何帮助,因此不会生成任何代码。


最好让编译器始终检查调试代码。
乔纳森·勒夫勒

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)

1

因此,使用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

然后,您可以分析对什么表达式进行了评估。

它可以防止双重评估问题,但是缺少gensyms确实会使它容易受到名称冲突的影响。

但是它确实嵌套:

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作为变量名,就可以了。

当然,我发现它(以及字符串的关联版本和调试级别的版本等)非常有价值。


1

多年来,我一直在思考如何执行此操作,最后提出了一个解决方案。但是,我不知道这里已经有其他解决方案了。首先,与Leffler的回答不同,我看不到他关于调试打印应该始终进行编译的说法。在不需要测试的情况下,我宁愿不要在我的项目中执行大量不需要的代码,以防我需要测试并且可能没有对它们进行优化。

每次都不编译可能听起来比实际更糟。您确实会得到有时不会编译的调试打印,但是在完成项目之前,对其进行编译和测试并不难。在此系统中,如果您使用的是三个调试级别,则只需将其放在调试消息级别为三个级别,修复编译错误并在完成代码之前检查其他任何错误。(当然,由于调试语句的编译不能保证它们仍按预期运行。)

我的解决方案还提供了调试详细信息的级别;如果将其设置为最高级别,它们都会编译。如果您最近一直在使用较高的调试详细信息级别,那么它们当时都可以编译。最终更新应该很容易。我从来不需要超过三个级别,但乔纳森说他已经使用了九个级别。这种方法(像Leffler方法一样)可以扩展到任意多个级别。我方法的使用可能更简单;在代码中使用时仅需要两个语句。但是,我也正在编码CLOSE宏-尽管它没有任何作用。如果我要发送到文件,则可能会出现。

相对于成本,测试它们以确保它们可以在交付之前进行编译的额外步骤是:

  1. 您必须信任它们才能进行优化,如果您有足够的优化级别,则必须这样做。
  2. 此外,如果您出于测试目的而关闭了优化功能的情况下进行发行版编译(这是罕见的),那么它们可能不会。而且几乎可以肯定它们在调试期间完全不会-从而在运行时执行数十或数百个“ if(DEBUG)”语句;因此减慢了执行速度(这是我的主要反对意见),而更不重要的是,增加了可执行文件或dll的大小;因此执行和编译时间。乔纳森(Jonathan)告诉我,他的方法也可以完全不编译语句。

实际上,在现代的预取处理器中,分支的成本相对较高。如果您的应用程序不是时间紧迫的应用程序,那么可能并不重要。但是如果性能是一个问题,那么是的,我想选择执行速度更快的调试代码(在少数情况下,可能会发布得更快,在极少数情况下,如上所述)就足够了。

因此,我想要的是一个调试打印宏,如果不进行打印,它不会编译,而如果不打印,它会编译。我还需要调试级别,例如,如果我希望代码中对性能至关重要的部分在某些时候不打印,而在其他时间打印,则可以设置调试级别,并增加额外的调试打印。遇到一种实现调试级别的方法,该级别确定打印是否已编译。我是这样实现的:

DebugLog.h:

// FILE: DebugLog.h
// REMARKS: This is a generic pair of files useful for debugging.  It provides three levels of 
// debug logging, currently; in addition to disabling it.  Level 3 is the most information.
// Levels 2 and 1 have progressively more.  Thus, you can write: 
//     DEBUGLOG_LOG(1, "a number=%d", 7);
// and it will be seen if DEBUG is anything other than undefined or zero.  If you write
//     DEBUGLOG_LOG(3, "another number=%d", 15);
// it will only be seen if DEBUG is 3.  When not being displayed, these routines compile
// to NOTHING.  I reject the argument that debug code needs to always be compiled so as to 
// keep it current.  I would rather have a leaner and faster app, and just not be lazy, and 
// maintain debugs as needed.  I don't know if this works with the C preprocessor or not, 
// but the rest of the code is fully C compliant also if it is.

#define DEBUG 1

#ifdef DEBUG
#define DEBUGLOG_INIT(filename) debuglog_init(filename)
#else
#define debuglog_init(...)
#endif

#ifdef DEBUG
#define DEBUGLOG_CLOSE debuglog_close
#else
#define debuglog_close(...)
#endif

#define DEBUGLOG_LOG(level, fmt, ...) DEBUGLOG_LOG ## level (fmt, ##__VA_ARGS__)

#if DEBUG == 0
#define DEBUGLOG_LOG0(...)
#endif

#if DEBUG >= 1
#define DEBUGLOG_LOG1(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG1(...)
#endif

#if DEBUG >= 2
#define DEBUGLOG_LOG2(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG2(...)
#endif

#if DEBUG == 3
#define DEBUGLOG_LOG3(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG3(...)
#endif

void debuglog_init(char *filename);
void debuglog_close(void);
void debuglog_log(char* format, ...);

DebugLog.cpp:

// FILE: DebugLog.h
// REMARKS: This is a generic pair of files useful for debugging.  It provides three levels of 
// debug logging, currently; in addition to disabling it.  See DebugLog.h's remarks for more 
// info.

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

#include "DebugLog.h"

FILE *hndl;
char *savedFilename;

void debuglog_init(char *filename)
{
    savedFilename = filename;
    hndl = fopen(savedFilename, "wt");
    fclose(hndl);
}

void debuglog_close(void)
{
    //fclose(hndl);
}

void debuglog_log(char* format, ...)
{
    hndl = fopen(savedFilename,"at");
    va_list argptr;
    va_start(argptr, format);
    vfprintf(hndl, format, argptr);
    va_end(argptr);
    fputc('\n',hndl);
    fclose(hndl);
}

使用宏

要使用它,只需执行以下操作:

DEBUGLOG_INIT("afile.log");

要写入日志文件,只需执行以下操作:

DEBUGLOG_LOG(1, "the value is: %d", anint);

要关闭它,您可以执行以下操作:

DEBUGLOG_CLOSE();

从技术上来讲,尽管目前这甚至是没有必要的,因为它什么也没做。我现在仍在使用CLOSE,以防万一我改变主意,希望在记录语句之间使文件保持打开状态。

然后,当您要打开调试打印时,只需在头文件中编辑第一个#define即可,例如

#define DEBUG 1

要使日志记录语句编译为空,请执行

#define DEBUG 0

如果您需要频繁执行的代码(即详细信息)中的信息,则可能需要编写:

 DEBUGLOG_LOG(3, "the value is: %d", anint);

如果将DEBUG定义为3,则会编译日志记录级别1、2和3。如果将其设置为2,则将获得日志记录级别1和2。如果将其设置为1,则将仅获得日志记录级别1语句。

至于do-while循环,由于它的计算结果为单个函数或为空,而不是if语句,因此不需要循环。好的,谴责我使用C而不是C ++ IO(并且在Qt中使用Qt的QString :: arg()也是一种更安全的格式化变量的方式-它很漂亮,但是需要更多的代码,并且格式化文档的组织性不强可能会-但仍然可以找到更适合的情况),但是您可以将所需的任何代码放入.cpp文件中。它也可能是一个类,但随后您需要实例化它并跟上它,或者执行new()并将其存储。这样,您只需将#include,init和可选的close语句放入源中,就可以开始使用它了。但是,如果您愿意的话,这将是一个很好的选择。

我以前见过很多解决方案,但没有一个比我的标准更适合我的标准了。

  1. 它可以扩展为执行任意多个级别。
  2. 如果不打印,它将编译为空。
  3. 它将IO集中在一个易于编辑的位置。
  4. 使用printf格式很灵活。
  5. 同样,它不会减慢调试运行的速度,而始终编译的调试打印始终以调试模式执行。如果您正在从事计算机科学,并且不容易编写信息处理程序,则可能会发现自己正在运行耗时CPU的模拟器,以查看调试器在哪里停止了索引,而索引超出了向量范围。这些已经在调试模式下运行得非常慢。强制执行数百个调试打印必将进一步减慢此类运行的速度。对我来说,这样的奔跑并不罕见。

并不十分重要,但另外:

  1. 它不需要任何技巧就可以在没有参数的情况下进行打印(例如DEBUGLOG_LOG(3, "got here!"););因此允许您使用,例如Qt的更安全的.arg()格式。它适用于MSVC,因此可能适用于gcc。正如Leffler指出的那样,它###defines中使用,它是非标准的,但得到了广泛的支持。(您可以重新编码它,使其##在必要时不使用,但您必须使用他提供的技巧。)

警告:如果您忘记提供日志记录级别参数,则MSVC会毫无帮助地声明未定义标识符。

您可能要使用除DEBUG以外的预处理器符号名称,因为某些来源也定义了该符号(例如,使用 ./configure命令进行编译的)。对我来说,开发它似乎很自然。我是在DLL被其他人使用的应用程序中开发的,将日志打印文件发送到文件更方便。但是将其更改为vprintf()也可以正常工作。

我希望这可以避免许多人为找出调试日志的最佳方法而感到悲伤。或向您显示您可能更喜欢的一种。数十年来,我一直全心全意地试图弄清楚这一点。适用于MSVC 2012和2015,因此可能适用于gcc;以及可能在许多其他产品上工作,但我尚未在它们上进行测试。

我的意思是也要制作这一天的流媒体版本。

注意:感谢Leffler,他诚挚地帮助我为StackOverflow设置了更好的消息格式。


2
您说“ if (DEBUG)在运行时执行数十或数百条语句,而这些语句不会得到优化”-这在风车中是很明显的。我所描述的系统的全部要点是,代码是由编译器检查的(重要且自动的-不需要特殊的构建),但调试代码完全不生成,因为它已被优化(因此对运行时的影响为零)代码大小或性能,因为代码在运行时不存在。
Jonathan Leffler

乔纳森·莱夫勒(Jonathan Leffler):谢谢指出我的措辞错误。我很高兴自己的想法能胜过我的手指。我用“ ...来修改我的异议” ... 1)您必须信任它们才能进行优化,如果您具有足够的优化级别,那肯定会发生。2)此外,如果您使用优化进行发布,它们也不会实现。出于测试目的而关闭;它们可能根本不会在调试过程中完全打开,从而在运行时执行数十或数百个“ if(DEBUG)”语句,从而增加了可执行文件或dll的大小以及执行时间。”
CodeLurker

为了使您的工作成为我的另一项重要工作,您必须具有调试级别。尽管我经常不需要打开很多应用程序,但是通过使用简单的“ #define DEBUG 3”能够获得有关时间关键循环的大量详细信息,然后再返回到“ #define DEBUG 1”的详细信息要少得多。我从来不需要超过三个级别,因此,至少有大约1/3的调试已在发行时编译。如果我最近使用过3级,那么他们可能都会使用。
CodeLurker

YMMV。我展示的现代系统支持调试级别的动态(运行时)设置,因此您可以以编程方式决定在运行时产生多少调试。我通常使用1-9级别,尽管没有上限(或下限;默认级别是0,通常不可用),但可以在活动开发过程中明确提出要求(如果合适,这不适用于长期工作)。我选择默认级别3;事情可以调整。这给了我很多控制权。如果您真的不想在不活动时测试调试代码,则将替代方法更改为((void)0)-这很容易。
乔纳森·莱夫勒

1
啊 阅读完整的内容会有所帮助。这是一个相当长的职位。我认为到目前为止,这已经很重要了。事实证明,您的像我一样可以用于编译或不编译所有调试打印,并且可以支持级别。尽管诚然,您可以编译不使用的级别-在调试期间需要付出一定的代价。
CodeLurker

0

我相信主题的这种变化形式提供了调试类别,而无需为每个类别使用单独的宏名称。

我在一个Arduino项目中使用了这种变体,其中程序空间限制为32K,动态内存限制为2K。添加调试语句和跟踪调试字符串会很快占用空间。因此,至关重要的是,每次编译代码时,都必须将编译时包含的调试跟踪限制为所需的最少数量。

调试

#ifndef DEBUG_H
#define DEBUG_H

#define PRINT(DEBUG_CATEGORY, VALUE)  do { if (DEBUG_CATEGORY & DEBUG_MASK) Serial.print(VALUE);} while (0);

#endif

调用.cpp文件

#define DEBUG_MASK 0x06
#include "Debug.h"

...
PRINT(4, "Time out error,\t");
...
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.