自定义封装前首先了解几个函数的原理
int printf(char *format [,argument,…]); //输出到标准输出流
最基础的将占位符替换为变量后输出到标准输出流中
int vprintf(const char * format, va_list arg args); //将变长参数拼接好之后输出到标准输出流
仔细思考一下作为printf的参数是一个变长的不可控数量的参数,因此再函数实现中想要完整的获取到参数就需要使用到va_list
va_list是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;
可变参数中的每个参数的类型可以不同,也可以相同;
可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。
va_list表示可变参数列表类型,实际上就是一个char指针fmt。
读到这里就可以开始自己试着封装自己的printf()函数了,下面是一个实例函数:
int LogPrint::printStr(const char *fmt, ...) const
{
struct timeval tv;
gettimeofday(&tv, nullptr);
struct tm *lt = localtime(&tv.tv_sec);
int ret = 0;
::printf("[%04d-%02d-%02d %02d:%02d:%02d:%03ld] ", //注意末尾保留一个空格
lt->tm_year + 1900,
lt->tm_mon + 1,
lt->tm_mday,
lt->tm_hour,
lt->tm_min,
lt->tm_sec,
tv.tv_usec / 1000);
va_list args;
va_start(args, fmt);
ret = vprintf(fmt, args); // to stdout
va_end(args);
return (logHeaderLength_ + ret);
}
这样就可以轻松的自定义一个带时间戳的打印函数了
int sprintf(char *string, char *format [,argument,…]);'
sprintf();函数的用法比前面两个函数高级了一些,它可以把你想要打印的带占位符的字符串,还有多个可变参数,最终输出到一个字符串指针中,这样我们就可以不用输出到标准输出流,可以随意使用此函数进行字符串的格式化。
得到组合完成的字符串,想干啥干啥
int vsprintf( char *buffer, char *format, va_list arg_ptr );
此函数结合了vprintf()和sprintf(),再这三个参数中,它可以把format字符串与后面的va_list中的变长参数整合后最终将整合好的字符串,整个的输出给buffer中,这样我们就得到了一个整合了变长参数,占位符合并完毕后的一个字符串,此时真的是想干啥干啥
编码中的实际bug
log4cpp::Category &root_;
void EmsLog::debug(const char *fmt, ...)
{
char strMsg[4096];
va_list args;
va_start(args, fmt);
sprintf(strMsg,fmt, args);
root_.debug(strMsg);
}
//此函数目的是想整合变长参数到一个字符串中然后交给log4cpp中的接口去处理,很明显这样写bug有很多就不一一分析了
修复版本
void EmsLog::debug(const char *fmt, ...)
{
char strMsg[4096];
int ret = 0;
//格式化拼接
va_list args;
va_start(args, fmt);
ret = vsprintf(strMsg,fmt, args);
va_end(args);
//传出
if(ret <= 4096){
strMsg[ret] = '\0';
root_.debug(strMsg);
}
}
😄一开始拿到bug代码一脸懵,后来通过段错误的反馈,以及同事江哥的指导下找到了这几个函数之间细微的区别,sprintf()与vsprintf()只是相差了一个字母,但足以让项目崩掉,c++编码还是要细,越是大佬越是注重细节。