-
函数原型
include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
str: 目的内存空间地址;
size: 最大可用的目的内存空间大小;
format:格式化字符串(类似printf);
... : 可选参数,与format中的占位符相关(类似printf).
-
函数功能
该函数可以将不同的内容(ex. int)按照期望的既定格式打印成字符串,并保存到buffer中。
比如需要将十进制数 20 转换为两bytes十六进制字符串的时候,可以使用该函数实现。
int ret;
char hex[10];
ret = snprintf(hex, sizeof(hex), "0X%04X", 20);
"0X%04X" 表示会先加上0X前缀,通常用来表示是十六进制;后面%04X是指按大写十六进制打印,且需要占据4位字符的宽度,不够4位时补0。所以最终的字符串格式会是“0X0014”。
-
特别注意
1. 无论何时一定会在字符串末尾添加'\0' 字符串结束符。
- 当字符串长度小于size时,会直接在字符串的末尾添加'\0';
- 当字符串长度等于size时,会取前size-1个字符,并在末尾添加‘\0’(即:加上'\0'后,一共size个字符);
- 当字符串长度大于size时,只取前size-1个字符,并在末尾添加'\0'(即:加上'\0'一共size个字符);
综上:snprintf不仅一定会自动在字符串末尾添加'\0',而且一定会尊重指定长度size的大小,不会超过该长度(算上自动在末尾增加的'\0')。所以从这一点来讲,使用snprintf是相对比较安全的,不用担心会出现overflow的情况。
NOTE:这一点上,snprintf与strncpy是有区别的。strncpy虽然也会指定目的内存的大小,但是当源字符串长度大于指定的内存大小时,strncpy虽然在达到指定大小后会停止copy,但是并不会在末尾自动添加'\0'。这样,在访问字符串时,有可能出现overflow的情况,因为末尾没有字符串结束符'\0'。
思考:
(1)当目的内存实际长度为N1,但是在使用snprintf时指定的实际size(第二个参数)为N2, 且N2<N1,可能会出现什么情况?
(2)如果N2>N1,又可能出现什么情况?
2. 特别小心返回值。
- 当出现错误时,会返回负值;
- 当字符串长度小于size时,返回打印到目的内存的实际字符串长度(不包括'\0');
- 当字符串长度大于等于size时,尽管字符串会被截断(只有size-1个字符串被打印到目的内存),但是返回值却会返回源字符串的实际长度(即假设目的内存无限大,总是能写下源字符串)。
综上:snprintf的返回值可能大于或等于指定的size,这时候说明目的内存不够大,源字符串被截断,需要小心处理,这是否是期望发生的情形。
NOTE:有一些写法会利用snprintf的这个特点来确定合适的目的内存大小,例如:
const char *fmt = "sqrt(2) = %f"; char * buf; int sz = snprintf(NULL, 0, fmt, sqrt(2)); buf = (char *)malloc(sz+1); // +1 for append '\0' snprintf(buf, sz+1, fmt, sqrt(2));
-
实践
在实际使用snprintf函数时,应该根据自己的实际需求,来判断如何合理的使用。
通常的用法会类似如下。
include <stdio.h>
include <stdlib.h>
#define BUF_LEN 20
char buf[BUF_LEN];
int year = 2019;
int month = 12;
int day = 13;
int ret;
ret = snprintf(buf, sizeof(buf), "%d-%02d-%02d", year, month, day);
if(ret <0) {
// error handling
} else if(ret >= sizeof(buf)) {
// buffer isn't enough, error handling
} else {
// expected case
}
-
更进一步
snprintf是一个在C99才被加入如标准的函数,原来的各个编译器都有自己的实现,至少.NET2003编译器还要是使用_snprintf这样的函数名称。而这些编译器间都有差异,而且Glibc库又有自己的不同的实现。如果使用一些比较旧的编译器或者不同平台的编译器,你会发现,如果传递的buf的长度不够的情况下,有可能null-terminator都没有加入,那么你使用的时候还是可能溢出,而且返回值的判断在不同的平台也可能不一样。
所以如果你想编一个可以跨平台的安全且正确的snprintf,应该怎么做呢?
以下只是一个可能的参考。
#define MAX_LEN 32
char buffer[MAX_LEN];
char* source = "xxxxxxx";
size_t buf_len = sizeof(buffer) - 1;
int len = snprintf(buffer, buf_len, "%s", source);
if ((len < 0) || (len > max_len))
{
// error handling
....
printf("error or overflow!\n");
}
else
{
// expected case
buffer[max_len] = 0;
}
参考文章:
[1] https://linux.die.net/man/3/snprintf
[2] http://joequery.me/code/snprintf-c/
[3] https://cloud.tencent.com/developer/article/1546579
[4] https://cloud.tencent.com/developer/article/1021143