在weibo上看到Laruence大神修复了一个使用snprintf的bug (http://t.cn/Rm6AuFh) 引起了TK教主的关注。TK教主着重提到了在windows下snprintf与_snprintf的行为有差别。
想想自己之前也在windows下写过代码,因具体的使用场景没有触发这种差异,因而对此也没有特别留意。下面对此写代码和查MSDN了具体验证了差别,结果记录如下。
先说snprintf,相信只要写过C代码的程序员,肯定用过这个C库函数,其声明如下
int snprintf(char *str, size_t size, const char *format, ...);
其向 str 为起始地址,长度为 size 的buffer中,按 format 指定的格式进行格式化写入串。size 限制了最大向 str 写入的字节数,但具体根据 format 格式和给定的额外参数,实际拼接出的串长度会超过 size。此时的结果就需要特别注意。man中对snprintf的说明如下
Upon successful return, these functions return the number of characters printed (excluding the null byte used to end output to strings).
The functions snprintf() and vsnprintf() do not write more than size bytes (including the terminating null byte (' ')). If the output was truncated due to
this limit, then the return value is the number of characters (excluding the terminating null byte) which would have been written to the final string if
enough space had been available. Thus, a return value of size or more means that the output was truncated. (See also below under NOTES.)
返回值是要特别注意的,并不是是向 str 写入的字节数,而是 format 和对应实参拼接出的串长度(称为len, 不包含尾部0字节)。
当 len 小于 size 时,除了将该串写入buffer之外,还额外追加一个0字节。
当 len 等于 size 时,将该串写入buffer之后,会将最后字节清零,该串位内容未字节也就被截断了。
当 len 大于 size 时,最多写入该串前(size-1)个字节,并将第 size 个字节清零。
snprintf 在 linux 下(libc-2.23.so) 和 windows 下(VS2015 VCRUNTIME140)行为一致,都是如上所述。
但在windows下 _snprintf 的行为和snprintf不完全一致。
当 len 小于 size 时,除了将该串写入buffer之外,还额外追加一个0字节,和 snprintf 相同
当 len 等于 size 时,直接将串写入buffer,并不会对尾字节清零,返回值为 len,和 snprintf 不同
当 len 大于 size 时,直接将串写入buffer,并不将第 size 个字节清零,返回值为 -1,和 snprintf 不同
MSDN对应的说明如下
Let len be the length of the formatted data string, not including the terminating null. len and count are in bytes for _snprintf, wide characters for _snwprintf.
If len < count, len characters are stored in buffer, a null-terminator is appended, and len is returned.
If len = count, len characters are stored in buffer, no null-terminator is appended, and len is returned.
If len > count, count characters are stored in buffer, no null-terminator is appended, and a negative value is returned.
实际使用如下代码进行测试,结果如下,注释中是 GDB 查看的中间结果。
#include <stdio.h>
#include <string.h>
#ifdef WIN32
#define snprintf _snprintf
#endif
int main(int argc, char *argv[])
{
char buffer[16];
int ret;
/*
(gdb) p ret
$1 = 4
(gdb) x /16bx buffer
0x7ffffffde250: 0x31 0x32 0x33 0x34 0x00 0x01 0x01 0x01
0x7ffffffde258: 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
*/
memset(buffer, 1, sizeof(buffer));
ret = snprintf(buffer, 8, "%s", "1234");
/*
(gdb) p ret
$2 = 8
(gdb) x /16bx buffer
0x7ffffffde250: 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x00
0x7ffffffde258: 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
*/
memset(buffer, 1, sizeof(buffer));
ret = snprintf(buffer, 8, "%s", "12345678");
/*
(gdb) p ret
$3 = 9
(gdb) x /16bx buffer
0x7ffffffde250: 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x00
0x7ffffffde258: 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01
*/
memset(buffer, 1, sizeof(buffer));
ret = snprintf(buffer, 8, "%s", "123456789");
return 0;
}
而在VS下,查看的中间结果, snprintf 和 _snprintf 分别如下
snprintf
ret = 4
buffer: 31 32 33 34 00, 01 01 01 01 01 01 01 01 01 01 01
ret = 8
buffer: 31 32 33 34 35 36 37 00, 01 01 01 01 01 01 01 01
ret = 9
buffer: 31 32 33 34 35 36 37 00, 01 01 01 01 01 01 01 01
_snprintf
ret = 4
buffer: 31 32 33 34 00, 01 01 01 01 01 01 01 01 01 01 01
ret = 8
buffer: 31 32 33 34 35 36 37 38, 01 01 01 01 01 01 01 01
ret = -1
buffer: 31 32 33 34 35 36 37 38, 01 01 01 01 01 01 01 01