如果您使用 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/tpop 和 http://cm.bell-labs.com/cm/cs/tpop 但现在 (2015-08-10) 都已损坏。
GitHub 中的代码
如果你好奇,可以在我的SOQ(堆栈
溢出问题)存储库中的文件debug.c、debug.h 和mddebug.c
src/libsoq
子目录。