gpt4 book ai didi

c - #define 宏用于 C 中的调试打印?

转载 作者:行者123 更新时间:2023-12-01 16:15:02 25 4
gpt4 key购买 nike

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

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

这是如何通过宏实现的?

最佳答案

如果您使用 C99 或更高版本的编译器

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

它假定您使用的是 C99(早期版本不支持变量参数列表表示法)。 do { ... } while (0) idiom 确保代码的行为就像一个语句(函数调用)。代码的无条件使用确保编译器始终检查您的调试代码是否有效——但优化器将在 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 之类的语句。这对于长期代码很重要——将持续一两年的代码。

假设一段代码大部分处于休眠状态(稳定)多年,但现在需要更改。您重新启用调试跟踪 - 但必须调试调试(跟踪)代码令人沮丧,因为它指的是在稳定维护的几年中已重命名或重新键入的变量。如果编译器(后预处理器)总是看到打印语句,它可以确保任何周围的更改都没有使诊断无效。如果编译器没有看到 print 语句,它就无法保护您免受您自己的粗心(或您的同事或合作者的粗心)的影响。见' The Practice of Programming ' 由 Kernighan 和 Pike 撰写,尤其是第 8 章(另见维基百科 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 或更高版本的单参数变体

凯尔·布​​兰特问道:

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-only 解决方案也提供了支持。

但是,您可以通过使用直接的 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 循环?

What's the purpose of the do while here?



您希望能够使用宏,使其看起来像一个函数调用,这意味着它将后跟一个分号。因此,您必须打包宏体以适应。如果您使用 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);

不幸的是,该缩进并不能反射(reflect)对流程的实际控制,因为预处理器生成与此等效的代码(缩进并添加大括号以强调实际含义):
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) loop 避免了这两个问题。

还有另一种编写宏的方法可能有效:
/* BAD - BAD - BAD */
#define debug_print(...) \
((void)((DEBUG) ? fprintf(stderr, __VA_ARGS__) : 0))

这使得程序片段显示为有效。 (void) cast 防止它在需要值的上下文中使用 - 但它可以用作逗号运算符的左操作数,其中 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 中查看 GitHub 中的这段代码。 (堆栈
溢出问题)存储库作为文件 debug.c , debug.hmddebug.c
src/libsoq
子目录。

关于c - #define 宏用于 C 中的调试打印?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1644868/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com