【问题标题】:#define macro for debug printing in C?#define 用于在 C 中调试打印的宏?
【发布时间】:2010-12-11 07:40:35
【问题描述】:

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

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

这是如何用宏来完成的?

【问题讨论】:

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

标签: c c-preprocessor


【解决方案1】:

如果你不关心输出到标准输出,你可以使用这个:

int doDebug = DEBUG;  // Where DEBUG may be supplied in compiler command
#define trace if (doDebug) printf

trace("whatever %d, %i\n", arg1, arg2);

【讨论】:

    【解决方案2】:
    #define PRINT_LOG(str_format, ...) { \
        time_t curtime=time (NULL); \
        struct tm *ltm = localtime (&curtime); \
        printf("[%d-%02d-%02d %02d:%02d:%02d] " str_format, \
            ltm->tm_year + 1900, ltm->tm_mon + 1, ltm->tm_mday, \
            ltm->tm_hour, ltm->tm_min, ltm->tm_sec, ##__VA_ARGS__); \
    }
        
    PRINT_LOG("[%d] Serving client, str=%s, number=%d\n", getpid(), "my str", 10);
    

    【讨论】:

    • 感谢您提供此代码 sn-p,它可能会提供一些有限的即时帮助。 proper explanation 将通过展示为什么这是解决问题的好方法,并使其对有其他类似问题的未来读者更有用,从而大大提高其长期价值。请编辑您的答案以添加一些解释,包括您所做的假设。
    【解决方案3】:

    如果您使用 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__ 技术更简洁,因为它使用常规函数表示法,而不是双括号 hack。

    为什么编译器总是能看到调试代码很重要?

    [对另一个答案进行重新散列。]

    上面的 C99 和 C89 实现背后的一个中心思想是,编译器本身总是看到调试 printf-like 语句。这对于长期代码很重要——可以持续一两年的代码。

    假设一段代码大部分时间都处于休眠状态(稳定)多年,但现在需要更改。您重新启用调试跟踪 - 但必须调试调试(跟踪)代码令人沮丧,因为它指的是在稳定维护期间已重命名或重新键入的变量。如果编译器(后预处理器)总是看到 print 语句,它确保任何周围的变化都没有使诊断无效。如果编译器没有看到 print 语句,它就不能保护您免受您自己的粗心(或您的同事或合作者的粗心)的影响。请参阅 Kernighan 和 Pike 的“The Practice of Programming”,尤其是第 8 章(另请参阅 Wikipedia on 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 问:

    无论如何,即使没有参数,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()”(但参数列表中的尾随逗号为@987654348 @ 将无法编译)。检查丢失是否是一个问题是有争议的。

    针对单个参数的 GCC 特定技术

    一些编译器可能会为处理宏中可变长度参数列表的其他方式提供扩展。具体来说,正如Hugo Ideler 在 cmets 中首次指出的那样,GCC 允许您省略通常出现在宏的最后一个“固定”参数之后的逗号。它还允许您在宏替换文本中使用##__VA_ARGS__,当但仅当前一个标记是逗号时,它会删除符号前面的逗号:

    #define debug_print(fmt, ...) \
                do { if (DEBUG) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0)
    

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

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

    如果你好奇,可以在我的SOQ(堆栈 溢出问题)存储库中的文件debug.cdebug.hmddebug.c src/libsoq 子目录。

    【讨论】:

    • 多年后,这个答案仍然是所有互联网中最有用的,关于如何给 printk 取别名! vfprintf 在内核空间中不起作用,因为 stdio 不可用。谢谢! #define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0)
    • 在您使用关键字__FILE__, __LINE__, __func__, __VA_ARGS__ 的示例中,如果您没有printf 参数,即如果您只调用debug_print("Some msg\n");,它将无法编译您可以使用fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__); 来解决此问题## __VA_ARGS__ 不允许向函数传递任何参数。
    • @LogicTom:区别在于#define debug_print(fmt, ...)#define debug_print(...)。其中第一个需要至少一个参数,即格式字符串 (fmt) 和零个或多个其他参数;第二个总共需要零个或多个参数。如果你在第一个中使用debug_print(),你会从预处理器中得到一个关于滥用宏的错误,而第二个没有。但是,您仍然会遇到编译错误,因为替换文本不是有效的 C。因此,它实际上并没有太大区别——因此使用了“有限检查”一词。
    • @JonathanLeffler 恐怕,我需要你的更多解释,handles the printing itself 是什么意思,你为什么提到flockfile()
    • 上面显示的变体@St.Antario 在整个应用程序中使用单个活动调试级别,我通常使用命令行选项来允许在程序运行时设置调试级别。我还有一个可以识别多个不同子系统的变体,每个子系统都有一个名称和自己的调试级别,这样我就可以使用-D input=4,macros=9,rules=2将输入系统的调试级别设置为4,将宏系统设置为9(进行严格审查)和规则系统 2. 主题有无穷无尽的变化;使用适合你的任何东西。
    【解决方案4】:

    多年来,我一直在苦苦思考如何做到这一点,终于想出了一个解决方案。但是,我不知道这里已经有其他解决方案。首先,与Leffler's answer 不同,我看不到他认为应该始终编译调试打印的论点。我宁愿不要在我的项目中执行大量不需要的代码,当不需要时,以防我需要测试并且它们可能没有得到优化。

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

    我的解决方案还提供了调试细节级别;如果您将其设置为最高级别,它们都会编译。如果您最近一直在使用高调试详细级别,那么它们当时都能够编译。最终更新应该很容易。我从来不需要超过三个级别,但乔纳森说他用了九个。这种方法(如 Leffler 的)可以扩展到任意数量的级别。我的方法的使用可能会更简单一些;在您的代码中使用时只需要两个语句。然而,我也在编写 CLOSE 宏——尽管它什么也没做。如果我发送到一个文件,它可能会。

    相对于成本,测试它们以查看它们是否会在交付前编译的额外步骤是这样的

    1. 您必须相信他们会得到优化,如果您有足够的优化级别,这无疑应该发生。
    2. 此外,如果您出于测试目的在关闭优化的情况下进行发布编译(这确实很少见),它们可能不会;而且它们几乎可以肯定在调试期间根本不会——从而在运行时执行数十或数百个“if (DEBUG)”语句;从而减慢执行速度(这是我的主要反对意见),不太重要的是,增加了可执行文件或 dll 的大小;因此执行和编译时间。然而,乔纳森告诉我,他的方法也可以完全不编译语句。

    在现代预取处理器中,分支实际上是相当昂贵的。如果您的应用程序不是时间关键型应用程序,可能没什么大不了的;但如果性能是一个问题,那么,是的,这是一个足够大的问题,我宁愿选择执行速度更快的调试代码(如前所述,在极少数情况下可能会更快地发布)。

    所以,我想要的是一个调试打印宏,如果不打印则不编译,但如果打印则编译。我还想要调试级别,例如如果我希望代码的性能关键部分在某些时候不打印,而是在其他时候打印,我可以设置一个调试级别,并启动额外的调试打印。我遇到了一种实现调试级别的方法,它决定了是否印刷品是否已编译。我是这样实现的:

    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 循环,由于 this 计算结果为单个函数或什么都没有,而不是 if 语句,因此不需要循环。好的,谴责我使用 C 而不是 C++ IO(并且 Qt 的 QString::arg() 也是在 Qt 中格式化变量的一种更安全的方法——它非常漂亮,但是需要更多的代码并且格式化文档没有那么有条理可能是这样 - 但我仍然发现了它更可取的情况),但您可以将任何代码放入您想要的 .cpp 文件中。它也可能是一个类,但是您需要实例化它并跟上它,或者执行 new() 并存储它。这样,您只需将#include、init 和可选的关闭语句放入源代码中,就可以开始使用它了。不过,如果你愿意的话,它会成为一堂很好的课。

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

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

    不是很重要,但另外:

    1. 不带参数即可打印(例如DEBUGLOG_LOG(3, "got here!"););从而允许您使用,例如Qt 更安全的 .arg() 格式。它适用于 MSVC,因此可能适用于 gcc。它在#defines 中使用##,正如Leffler 指出的那样,这是非标准的,但得到了广泛的支持。 (如有必要,您可以将其重新编码为不使用##,但您必须使用他提供的hack。)

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

    您可能希望使用除 DEBUG 之外的预处理器符号名称,因为某些源也定义了该符号(例如 progs 使用 ./configure 命令准备构建)。当我开发它时,这对我来说似乎很自然。我在一个应用程序中开发了它,其中 DLL 被其他东西使用,并且将日志打印发送到文件更方便;但是将其更改为 vprintf() 也可以正常工作。

    我希望这可以避免你们中的许多人为找出进行调试日志记录的最佳方法而烦恼;或向您展示您可能更喜欢的一个。几十年来,我一直在半心半意地试图弄清楚这一点。在 MSVC 2012 和 2015 中工作,因此可能在 gcc 上工作;以及可能在许多其他人身上工作,但我还没有在他们身上测试过。

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

    注意:感谢 Leffler,他热心帮助我更好地为 StackOverflow 格式化我的消息。

    【讨论】:

    • 您说“在运行时执行数十或数百个if (DEBUG) 语句,这些语句没有得到优化”——即tilting at windmills。我描述的系统的全部要点是代码由编译器检查(重要且自动 - 不需要特殊的构建),但根本不会生成调试代码,因为它优化了(因此对代码大小或性能的运行时影响为零,因为代码在运行时不存在)。
    • Jonathan Leffler:感谢您指出我的错误措辞。我让我的想法比我的手指更快,很高兴完成了这件事。我已经修改了我的反对意见“... 1) 你必须相信他们会得到优化,如果你有足够的优化级别,这当然应该发生。2) 此外,如果你使用优化进行发布编译,他们不会出于测试目的而关闭;并且它们可能在调试期间根本不会 - 从而在运行时执行数十或数百个 'if (DEBUG)' 语句 - 从而增加您的可执行文件或 dll 大小和执行时间。"
    • 为了让你做我正在做的另一件重要的事情,你必须有调试级别。虽然我通常不需要打开很多,但一些应用程序确实受益于能够通过简单的“#define DEBUG 3”获得有关时间关键循环的大量细节,然后返回“#define DEBUG 1”的详细信息要少得多。我从来不需要超过三个级别,因此,至少大约 1/3 的调试已经在发布时编译。如果我最近用过 3 级,他们可能都用过。
    • YMMV。我展示的现代系统支持调试级别的动态(运行时)设置,因此您可以通过编程方式决定在运行时生成多少调试。我通常使用级别 1-9,虽然没有上限(或下限;默认级别是 0,通常是关闭的,但如果合适,可以在积极开发期间明确要求 - 它不适合长期工作)。我选择了默认级别 3;事情可以调整。这给了我很大的控制权。如果您真的不想在非活动状态下测试调试代码,请将替代项更改为 ((void)0) — 这很简单。
    • 啊。阅读整本书会有所帮助。这是一个相当长的帖子。我认为这是迄今为止的要点。事实证明,你的和我的一样,可以用来编译或不编译所有调试打印,并且可以支持关卡;虽然不可否认,你可以编译你不使用的关卡——在调试期间需要付出代价。
    【解决方案5】:

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

    当然,我发现它(以及字符串的相关版本,以及调试级别的版本等)非常宝贵。

    【讨论】:

      【解决方案6】:

      我相信主题的这种变体提供了调试类别,而无需为每个类别设置单独的宏名称。

      我在 Arduino 项目中使用了这种变体,其中程序空间限制为 32K,动态内存限制为 2K。添加调试语句和跟踪调试字符串会很快占用空间。因此,必须能够将编译时包含的调试跟踪限制在每次构建代码时所需的最低限度。

      调试.h

      #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");
      ...
      

      【讨论】:

        【解决方案7】:

        下面我最喜欢的是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)
        

        【讨论】:

          【解决方案8】:

          这是我用的:

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

          正确处理 printf 有很大的好处,即使没有额外的参数。在 DBG ==0 的情况下,即使是最愚蠢的编译器也无济于事,因此不会生成任何代码。

          【讨论】:

          • 最好让编译器始终检查调试代码。
          【解决方案9】:

          我使用这样的东西:

          #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
          

          【讨论】:

          • 至于for(;0;),写D continue;D break;之类的东西可能会出问题。
          • 找到我了;不过,这似乎不太可能发生意外。
          【解决方案10】:

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

          否则,宏 #define dbg_print(format, ...) printf(format, __VA_ARGS__) 将无法编译以下示例:dbg_print("hello world");

          【讨论】:

          • 欢迎来到 Stack Overflow。 GCC 具有您引用的非标准扩展是正确的。当前接受的答案实际上确实提到了这一点,包括您提供的参考 URL。
          【解决方案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;
          }
          

          或者(hackish,不推荐)

          #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;
          }
          

          【讨论】:

          • @LB:让预处理器“认为”只有一个参数,而让 _ 在稍后阶段扩展。
          【解决方案12】:

          这是我使用的版本:

          #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
          

          【讨论】:

            【解决方案13】:

            我会做类似的事情

            #ifdef DEBUG
            #define debug_print(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
            #else
            #define debug_print(fmt, ...) do {} while (0)
            #endif
            

            我认为这更干净。

            【讨论】:

            • 我不太喜欢在测试中使用宏作为标志的想法。你能解释一下为什么总是要检查调试打印吗?
            • @Jonathan:如果代码只在调试模式下执行,你为什么要关心它是否在非调试模式下编译?来自 stdlib 的assert() 工作方式相同,我通常只是将NDEBUG 宏重新用于我自己的调试代码......
            • 在测试中使用 DEBUG,如果有人做了一个不受控制的 undef DEBUG,你的代码将不再编译。对吗?
            • 启用调试令人沮丧,然后必须调试调试代码,因为它引用已重命名或重新键入的变量等。如果编译器(后预处理器)总是看到打印语句,它确保任何周围的变化都没有使诊断无效。如果编译器没有看到 print 语句,它就不能保护您免受您自己的粗心(或您的同事或合作者的粗心)的影响。请参阅 Kernighan 和 Pike 的“编程实践” - plan9.bell-labs.com/cm/cs/tpop
            • @Christoph:嗯,有点……我使用 NDEBUG 仅控制断言,并使用单独的宏(通常是 DEBUG)来控制调试跟踪。我经常不希望无条件地出现调试输出,所以我有控制输出是否出现的机制(调试级别,而不是直接调用 fprintf(),我调用了一个调试打印函数,它只有条件地打印,所以相同的构建代码可以根据程序选项打印或不打印)。我主张对于所有构建,编译器都应该看到诊断语句;但是,除非启用调试,否则它不会生成代码。
            【解决方案14】:
            #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
            • OK - 同意:它被记录为旧的 GNU 扩展(GCC 4.4.1 手册的第 5.17 节)。但是您可能应该记录它仅适用于 GCC - 或者我们在这些 cmets 中我们之间已经这样做了。
            • 我的目的是展示另一种使用 args 的方式,主要是为了演示 FUNCTIONLINE 的用法
            猜你喜欢
            • 2023-03-13
            • 2015-04-08
            • 1970-01-01
            • 1970-01-01
            • 2011-03-02
            • 1970-01-01
            • 2011-09-11
            相关资源
            最近更新 更多