对格式化字符串的一些思考

在最近工作中,在拼接sql语句时,用到格式化字符串,经过和同事讨论后,有了新的思考。这个点在之前都是参照已有来使用,没有深入去追究,本次全面了解下。

格式化字符串

在实际工程中,格式化字符串一般用在打印日志、拼接字符串等,目前在工程中常见的是这样用的:

char szbuf[128] = {0};
snprintf(szbuf, sizeof(szbuf) - 1, "log:%d %s", 1,"XXXX");

按照上述格式化完后,对缓冲区szBuf进行各种操作。这里面有几点疑问:

  1. 这里没有判断snprintf的返回值,需不需要判断?
  2. snprintf的第二个参数为什么要减1?
  3. 没有考虑格式化完整性

下面依次分析。

snprintf的返回值代表什么含义

linux环境上,snprintf的函数原型如下。

int snprintf(char* str, size_t size, const char* format, ...);

查阅snprintfman手册,对返回值有如下说明:

snprintf返回值说明.jpg

该函数的返回值表示待格式化字符串的长度,不包括结束分隔符。这是什么意思呢?举个例子:

  • 格式化 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的长度,但只能缓解拼接截断发生的概览,对于有动态数据的查询,还是要小心。

总结

本文总结了在格式化字符串时的一些注意事项,给出自认为比较正确的做法,并就格式化截断后的处理进行了简短讨论。

另外一点,在开发过程中,不能有照抄心态。这种心态,会参照现有工程中类似逻辑的做法,其他地方是怎么用的,所以这里也这么用。这种心态或者做法,不会使得代码质量有所提高,反而会复制粘贴可能的错误或不规范的用法。

posted @ 2020-12-21 14:32  浩天之家  阅读(188)  评论(0编辑  收藏  举报