对格式化字符串的一些思考
在最近工作中,在拼接sql语句时,用到格式化字符串,经过和同事讨论后,有了新的思考。这个点在之前都是参照已有来使用,没有深入去追究,本次全面了解下。
格式化字符串
在实际工程中,格式化字符串一般用在打印日志、拼接字符串等,目前在工程中常见的是这样用的:
char szbuf[128] = {0};
snprintf(szbuf, sizeof(szbuf) - 1, "log:%d %s", 1,"XXXX");
按照上述格式化完后,对缓冲区szBuf
进行各种操作。这里面有几点疑问:
- 这里没有判断
snprintf
的返回值,需不需要判断? snprintf
的第二个参数为什么要减1?- 没有考虑格式化完整性
下面依次分析。
snprintf的返回值代表什么含义
在linux
环境上,snprintf
的函数原型如下。
int snprintf(char* str, size_t size, const char* format, ...);
查阅snprintf
的man
手册,对返回值有如下说明:
该函数的返回值表示待格式化字符串的长度,不包括结束分隔符。这是什么意思呢?举个例子:
- 格式化
abcd
字符串,返回值是4,表示待格式化的字符个数为4。 - 格式化
01234567890
字符串,返回值为10。
这个返回值和传入的缓冲区长度size
没有任何关系的,它只与待格式化字符串的长度有关。
另一个需要注意的点是,这个函数在格式化时,不会往缓冲区str中写入多于size
大小的数据,包括\0
在内。这就是说,外部传入size
大小的缓冲区,该函数能够保证在size
大小内,填充包含\0
,至于有没有截断,需要使用者来进一步判断。
至此,可解答上面两个问题:
snprintf
的返回值表示待格式化字符串的长度snprintf
的第二个参数没必要减1,按照输入缓冲区长度传入即可。
第三个问题,判断格式化后是否被截断。从上述说明来看,如果返回值大于等于入参size
时,表明格式化输出被截断。另外一种情况,如果遇到输出错误,会返回一个负值。
因此,可总结出如下代码:
const int nBufSize = 256;
char szBuf[nBufSize] = {0};
int nRet = snprintf(szBbuf, nBufSize, "XXXX", "XXXX");
if (nRet >= nBufSize || nRet < 0)
{
// 格式化被截断或者格式化失败
}
else
{
// 正常格式化,走正常流程
}
截断后的处理
一般来说,我们在格式化字符串时,提供的缓冲区都足够大,保证不会发生截断。当有些格式化数据由外部传入,当这些数据含有异常、超长数据时,比如一个无效的double值,同时此时缓冲区较小,就可能会被截断。
截断后要如何处理呢?这里不好一概而论,得区分格式化字符串的上下文场景
-
如果是执行
sql
,这种情况下,无论被截断的是哪部分,这个sql
都不安全l,不允许后续使用。发生此种情况的截断,需要记录错误日志后退出。 -
如果是记录日志,日志是为了检查系统状态,即使被截断,前面信息也是有效的,这种场景也要记录为错误日志,但后续逻辑可继续运行。
以上是我目前想到的两种场景,可能还有其他场景。就比如执行sql
来说,如果是很简单,很显而易见的sql
,例如根据UserId
从数据库中查找相关信息,这种很简单的sql
语句,要不要判断截断呢?如果这种简单的不判断,那复杂到什么程度才判断呢?感觉这样判断,会让逻辑依赖于数据,这是不好的做法。笔者在这里无法给出绝对的结论,不同场景下不同的处理,能够达成团队一致即可。
这里说一句,即使使用存储过程,缩短sql
的长度,但只能缓解拼接截断发生的概览,对于有动态数据的查询,还是要小心。
总结
本文总结了在格式化字符串时的一些注意事项,给出自认为比较正确的做法,并就格式化截断后的处理进行了简短讨论。
另外一点,在开发过程中,不能有照抄心态。这种心态,会参照现有工程中类似逻辑的做法,其他地方是怎么用的,所以这里也这么用。这种心态或者做法,不会使得代码质量有所提高,反而会复制粘贴可能的错误或不规范的用法。