前言
在大型项目中使用printf
大法进行调试的时候,如果能同时输出所在行,那么分析log就会简单得多。
自制printf
首先自制一个printf
,要加上行号和文件名。
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
int vprintf2(const char* src_fn,const int src_ln,const char* format,va_list arg_list)
{
char buff[512]; // 记得调整缓冲区大小!
int a=snprintf(buff,sizeof(buff),"[%s@%d] ",src_fn,src_ln);
int b=vsnprintf(&buff[a],sizeof(buff)-a,format,arg_list);
// 可以把fwrite替换成别的输出,比如OutputDebugStringA
fwrite(buff,sizeof(char),a+b,stdout);
return a+b;
}
int printf2(const char* src_fn,const int src_ln,const char* format,...)
{
int ret;
va_list arg_list;
va_start(arg_list,format);
ret=vprintf2(src_fn,src_ln,format,arg_list);
va_end(arg_list);
return ret;
}
调用printf2
就这么写:
printf2(__FILE__,__LINE__,"Hello World!");
但是每次调用printf2
都要加上__FILE__
和__LINE__
就很蛋疼,而且我们还不能在vprintf2
的实现中直接放这两个宏,否则log中的文件名和行号永远是vprintf2这个函数的位置。
宏包装
不过我们可以用宏来包装printf2
:
#define printf3(fmt,...) printf2(__FILE__,__LINE__,fmt,__VA_ARGS__)
这个宏定义在MSVC是没问题的,但是在GCC和clang里用是有坑的。
举个例子,printf3("Hello");
在宏展开时会变成printf2(__FILE__,__LINE__,"Hello",);
,注意括号前多出了一个逗号。这是因为我们没有使用额外的参数所导致的。而MSVC允许在括号前多出一个逗号,因此仍然能编译通过。
要让GCC和clang对这种情况进行特殊处理,需要改变__VA_ARGS__
的使用方式:
#define printf3(fmt,...) printf2(__FILE__,__LINE__,fmt,##__VA_ARGS__)
虽然GNU说这是GCC的扩展语法,但是MSVC也支持这个语法。因此直接用上述的套路包装printf
即可。
安全隐患
在对外发布的时候,程序输出的日志是不可以泄漏源文件的信息的,因此需要用条件编译把printf3
包装起来,使得对外发布的版本里不包含源文件信息。
#if defined(_DEBUG)
#define printf3(fmt,...) printf2(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#else
#define printf3(fmt,...) printf(fmt,##__VA_ARGS__)
#endif