/*
@(#)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 */
此外,他们可能在调试期间赢得了't if you make a release compile with optimization turned off for testing purposes (which is admittedly rare); and they almost certainly won' t - 从而在运行时执行了数十或数百个"if (DEBUG)"语句;从而减慢执行速度(这是我的主要反对意见),不太重要的是,增加可执行文件或DLL的大小;因此执行和编译时间 . 然而,Jonathan告诉我他的方法也可以根本不编译语句 .
// 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);
}
12 回答
根据http://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html,在
__VA_ARGS__
之前应该有##
.否则,宏
#define dbg_print(format, ...) printf(format, __VA_ARGS__)
将无法编译以下示例:dbg_print("hello world");
.对于便携式(ISO C90)实现,您可以使用双括号,如下所示;
或者(hackish,不推荐它)
如果使用C99编译器
它假定您使用的是C99(早期版本不支持变量参数列表表示法) .
do { ... } while (0)
成语确保代码的作用类似于语句(函数调用) . 无条件使用代码可确保编译器始终检查您的调试代码是否有效 - 但优化器将在DEBUG为0时删除代码 .如果您想使用#ifdef DEBUG,请更改测试条件:
然后使用DEBUG_TEST我使用DEBUG .
如果你坚持使用格式字符串的字符串文字(无论如何都是个好主意),你也可以在输出中引入
__FILE__
,__LINE__
和__func__
这样的东西,这可以改善诊断:这依赖于字符串连接来创建比程序员编写的更大的格式字符串 .
如果使用C89编译器
如果你坚持使用C89并且没有有用的编译器扩展,那么就没有一种特别干净的方法来处理它 . 我以前使用的技术是:
然后,在代码中,写:
双括号是至关重要的 - 这也是为什么你在宏观扩张中有一个有趣的符号 . 和以前一样,编译器总是检查代码的语法有效性(这很好),但是如果DEBUG宏的计算结果为非零,优化器只会调用打印函数 .
这确实需要一个支持函数 - 示例中的dbg_printf() - 来处理'stderr'之类的东西 . 它需要你知道如何编写varargs函数,但这并不难:
当然,您也可以在C99中使用此技术,但
__VA_ARGS__
技术更整洁,因为它使用常规函数表示法,而不是双括号黑客 .为什么编译器始终看到调试代码至关重要?
[重新发表对另一个答案的评论 . ]
上面的C99和C89实现背后的一个中心思想是编译器本身总是看到调试类似printf的语句 . 这对于长期代码很重要 - 代码将持续十年或两年 .
假设一段代码多年来一直处于休眠状态(稳定),但现在需要进行更改 . 您重新启用调试跟踪 - 但是必须调试调试(跟踪)代码是令人沮丧的,因为它引用了在稳定维护期间已重命名或重新键入的变量 . 如果编译器(后预处理器)始终看到print语句,则它确保任何周围的更改都没有使诊断无效 . 如果编译器没有看到print语句,它就无法保护您免受自己的粗心(或同事或合作者的疏忽) . 请参阅Kernighan和Pike的'The Practice of Programming',尤其是第8章(另见维基百科TPOP) .
这就是“在那里,完成了”经验 - 我基本上使用了其他答案中描述的技术,其中非调试版本多年(超过十年)没有看到类似printf的语句 . 但是我在TPOP中看到了建议(参见我之前的评论),然后在几年后启用了一些调试代码,并遇到了更改上下文的问题,打破了调试 . 有几次,打印总是经过验证,这使我免于后来的问题 .
我使用NDEBUG仅控制断言,并使用单独的宏(通常为DEBUG)来控制是否在程序中内置调试跟踪 . 即使内置调试跟踪,我也经常不希望调试输出无条件出现,所以我有机制来控制输出是否出现(调试级别,而不是直接调用fprintf(),我调用一个调试打印函数,只有条件打印所以代码的相同版本可以打印或不打印基于程序选项) . 我还有一个用于更大程序的代码的“多子系统”版本,因此我可以让程序的不同部分产生不同的跟踪量 - 在运行时控制下 .
我主张对于所有构建,编译器应该看到诊断语句;但是,除非启用调试,否则编译器不会为调试跟踪语句生成任何代码 . 基本上,这意味着每次编译时编译器都会检查所有代码 - 无论是发布还是调试 . 这是件好事!
debug.h - 1.2版(1990-05-01)
debug.h - 版本3.6(2008-02-11)
单参数C99变体
凯尔勃兰特问道:
有一个简单,老式的黑客:
仅GCC解决方案也为此提供支持 .
但是,您可以使用直接C99系统来执行以下操作:
与第一个版本相比,您失去了需要'fmt'参数的有限检查,这意味着有人可以在没有参数的情况下调用'debug_print()' . 失去检查是否是一个问题是值得商榷的 .
GCC特定技术
有些编译器可能会为宏中处理可变长度参数列表的其他方法提供扩展 . 具体来说,正如Hugo Ideler在注释中首次提到的那样,GCC允许您省略通常出现在宏的最后一个'fixed'参数之后的逗号 . 它还允许您在宏替换文本中使用##VA_ARGS,如果,但前提是前一个标记是逗号,则删除符号前面的逗号:
此解决方案保留了在格式化后接受可选参数的同时需要format参数的好处 .
Clang也支持此技术,以实现GCC兼容性 .
为什么do-while循环?
您希望能够使用宏,因此它看起来像一个函数调用,这意味着它将跟随一个分号 . 因此,您必须打包宏体以适应 . 如果你使用没有周围
do { ... } while (0)
的if
语句,你将拥有:现在,假设你写道:
不幸的是,该缩进并不反映流的实际控制,因为预处理器生成与此等效的代码(为了强调实际含义而添加了缩进和大括号):
宏的下一次尝试可能是:
现在相同的代码片段产生:
并且
else
现在是语法错误 .do { ... } while(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(堆栈溢出问题)存储库中的GitHub中查看此代码,作为src/libsoq子目录中的文件
debug.c
,debug.h
和mddebug.c
.我使用这样的东西:
比我只使用D作为前缀:
编译器看到调试代码,没有逗号问题,它可以在任何地方使用 . 当
printf
不够时,它也可以运行,比如当你必须转储数组或计算一些对程序本身多余的诊断值时 .编辑:好的,当有一个
else
附近可能被这个注入的if
截获时,它可能会产生一个问题 . 这是一个覆盖它的版本:我会做点什么的
我认为这更清洁了 .
这是我使用的版本:
我最喜欢的是
var_dump
,当被称为:var_dump("%d", count);
产生如下输出:
patch.c:150:main(): count = 0
归功于@“Jonathan Leffler” . 所有人都满意C89:
Code
这是我用的:
即使没有额外的参数,它也能很好地处理printf . 如果DBG == 0,即使是最笨的编译器也没有任何东西可以咀嚼,因此不会生成任何代码 .
所以,在使用gcc时,我喜欢:
因为它可以插入代码中 .
假设您正在尝试调试
然后你可以把它改成:
您可以分析哪些表达式被评估为什么 .
它受到双重评估问题的保护,但缺少gensyms确实让它对名称冲突持开放态度 .
然而它确实嵌套:
所以我认为只要你避免使用g2rE3作为变量名,你就行了 .
当然,我发现它(和字符串的联合版本,以及调试级别的版本等)非常宝贵 .
我知道这里已经有其他解决方案了 . 首先,在与Leffler's answer的区别,我不需要在我的项目中执行大量不需要的代码,如果不需要,在我需要测试的情况下,他们可能没有得到优化 .
每次不编译可能听起来比在实际操作中更糟糕 . 您最终会得到有时无法编译的调试打印,但在最终确定项目之前编译和测试它们并不困难 . 使用此系统,如果您使用三个级别的调试,只需将其置于调试消息级别3,修复编译错误并在最终确定您的代码之前检查其他任何错误 . (当然,编译调试语句并不能保证它们仍然按预期工作 . )
我的解决方案也提供了调试细节级别;如果你把它设置到最高级别,它们都会编译 . 如果你最近一直在使用高调试细节级别,那么他们当时都能够编译 . 最终更新应该非常简单 . 我从来没有超过三个级别,但乔纳森说他用了九个级别 . 这种方法(如Leffler's)可以扩展到任意数量的级别 . 我的方法的用法可能更简单;在代码中使用时只需要两个语句 . 然而,我也在编写CLOSE宏 - 尽管它没有做任何事情 . 如果我发送到文件可能会 .
在成本的基础上,测试它们以确定它们将在交付之前进行编译的额外步骤就是这样
你必须相信它们才能得到优化,如果你有足够的优化水平,那么应该会发生这种情况 .
此外,他们可能在调试期间赢得了't if you make a release compile with optimization turned off for testing purposes (which is admittedly rare); and they almost certainly won' t - 从而在运行时执行了数十或数百个"if (DEBUG)"语句;从而减慢执行速度(这是我的主要反对意见),不太重要的是,增加可执行文件或DLL的大小;因此执行和编译时间 . 然而,Jonathan告诉我他的方法也可以根本不编译语句 .
在现代预取处理器中,分支实际上相对昂贵 . 如果你的应用不是一个时间紧迫的应用,也许不是什么大不了的事;但是,如果性能是一个问题,那么,是的,这是一个足够大的交易,我更愿意选择更快的执行调试代码(并且可能更快的发布,在极少数情况下,如上所述) .
所以,我想要的是一个调试打印宏,如果不打印它就不能编译,但如果是的话 . 我也想要调试级别,例如,如果我希望代码的性能关键部分不要在某些时候打印,而是在别人打印,我可以设置一个调试级别,并启动额外的调试打印 . 我遇到了一种实现调试级别的方法,确定是否打印甚至编译或不编译 . 我这样做了:
DebugLog.h:
DebugLog.cpp:
使用宏
要使用它,只需:
要写入日志文件,只需执行以下操作:
要关闭它,您可以:
虽然从技术上讲,目前这甚至不是必要的,因为它什么都不做 . 我现在仍然在使用CLOSE,但是,如果我改变主意它的工作原理,并希望在记录语句之间保持文件打开 .
然后,当你想打开调试打印时,只需编辑头文件中的第一个#define,例如,
要将日志记录语句编译为空,请执行
如果您需要经常执行的代码中的信息(即高级别的详细信息),您可能需要写:
如果将DEBUG定义为3,则编译日志级别1,2和3 . 如果将其设置为2,则会获得日志记录级别1和2.如果将其设置为1,则只能获取日志记录级别1语句 .
对于do-while循环,由于这会求值为单个函数或什么都不是,而不是if语句,因此不需要循环 . 好吧,谴责我使用C代替C IO(和Qt的QString :: arg()是一种更安全的格式化变量的方式,在Qt中也是如此 - 它很漂亮,但需要更多代码,而且格式化文档也没有那么有条理因为它可能 - 但我仍然发现它更可取的情况),但你可以把任何代码放在你想要的.cpp文件中 . 它也可能是一个类,但是你需要实例化它并跟上它,或者做一个new()并存储它 . 这样,您只需将#include,init和可选的close语句放入源代码中,就可以开始使用它了 . 如果你这么倾向,它会成为一个优秀的课程 .
我以前见过很多解决方案,但是没有一个适合我的标准 .
它可以扩展到你喜欢的多个级别 .
如果不打印则编译为空 .
它将IO集中在一个易于编辑的地方 .
使用printf格式很灵活 .
同样,它不会减慢调试运行速度,而始终编译调试打印始终在调试模式下执行 . 如果您正在进行计算机科学,并且不容易编写信息处理,您可能会发现自己正在运行一个消耗CPU的模拟器,例如,调试器在哪里使用索引超出矢量范围来停止它 . 这些在调试模式下已经非常缓慢地运行 . 数百个调试打印的强制执行必然会进一步降低这种运行速度 . 对我来说,这样的运行并不少见 .
不是非常重要,但另外:
DEBUGLOG_LOG(3, "got here!");
);从而允许你使用,例如Qt更安全.arg()格式化 . 它适用于MSVC,因此可能是gcc . 它在#define
中使用##
,这是非标准的,正如Leffler指出的那样,但得到了广泛的支持 . (如果需要,你可以重新编码它不要使用##
,但你必须使用他提供的黑客 . )警告:如果您忘记提供日志记录级别参数,MSVC无理由声明未定义标识符 .
您可能希望使用除DEBUG之外的预处理程序符号名称,因为某些源还定义了该符号(例如,使用
./configure
命令编写以准备构建) . 当我开发它时,我似乎很自然 . 我在一个应用程序中开发它,其中DLL被其他东西使用,并且它更可以将日志打印发送到文件;但将其更改为vprintf()也可以正常工作 .我希望这可以帮助您解决许多关于找出调试日志记录的最佳方法的悲痛 . 或者向您展示您可能更喜欢的 . 几十年来,我一直在努力想出这个问题 . 适用于MSVC 2012和2015,因此可能在gcc上;以及可能在许多其他人工作,但我没有测试它们 .
我的意思是制作这一天的流媒体版本 .
注意:感谢Leffler,他已经帮助我更好地为StackOverflow格式化我的消息 .
我相信主题的这种变化给出了调试类别,而不需要为每个类别分别设置一个宏名称 .
我在Arduino项目中使用了这种变体,其中程序空间限制为32K,动态内存限制为2K . 添加调试语句和跟踪调试字符串会快速占用空间 . 因此,必须能够将编译时包含的调试跟踪限制为每次构建代码时所需的最小值 .
debug.h
调用.cpp文件