第26问:字符串与数字转换——sprintf与snprintf、sscanf
字符串和数字之间的转换,是C语言开发中最常见的操作之一。我做了这么多年嵌入式开发,几乎每个项目都会遇到这类需求。说白了,就是把数字变成人能读懂的字符串,或者反过来从字符串里把数字提取出来。
今天咱们就聊聊 sprintf、snprintf 和 sscanf 这三个函数。它们看似简单,但坑不少。我见过太多同事在这上面栽跟头了。
sprintf:最原始,也最危险
sprintf 的作用很简单——把格式化的数据写入字符串缓冲区。原型长这样:
int sprintf(char *str, const char *format, ...);
它返回实际写入的字符数(不包括结尾的'\0')。
举个例子:
char buf[32];
int val = 12345;
sprintf(buf, "数值是:%d", val);
// buf 里现在存着 "数值是:12345"
看起来挺方便对吧?但问题来了——它不检查缓冲区长度。你想想看,如果格式化后的字符串超过了缓冲区大小,会发生什么?缓冲区溢出,直接写到相邻内存去了。这是典型的安全漏洞。
⚠️ 我曾经在一个通信协议解析模块里,看到同事用 sprintf 拼接一个很长的 JSON 字符串。缓冲区只给了 128 字节,但实际数据经常超过 200 字节。结果就是程序偶尔崩溃,查了两天才定位到问题。从那以后,我给自己定了个规矩:能用 snprintf 的地方,绝不用 sprintf。
snprintf:安全的选择
snprintf 是 sprintf 的安全版本。它多了一个参数——缓冲区大小:
int snprintf(char *str, size_t size, const char *format, ...);
关键点:
- 最多写入
size - 1个字符(留一个给'\0') - 返回值是如果缓冲区足够大会写入的字符数(不包括'\0')
- 如果返回值 ≥ size,说明数据被截断了
看个例子:
char buf[16];
int val = 123456789;
int len = snprintf(buf, sizeof(buf), "%d", val);
// buf 里是 "123456789"(9个字符 + '\0')
// len 是 9
// 如果数字更大:
int big = 1234567890123;
len = snprintf(buf, sizeof(buf), "%d", big);
// buf 里只有前15个字符
// len 是 13(实际需要的长度)
// 返回值 ≥ sizeof(buf),说明被截断了
💡 我个人习惯:每次调用 snprintf 后,都会检查返回值。如果返回值 ≥ 缓冲区大小,就说明数据被截断了,需要做相应处理。很多安全规范都要求这么做。
sscanf:反向解析
sscanf 是 sprintf 的逆操作——从字符串里按格式提取数据:
int sscanf(const char *str, const char *format, ...);
它返回成功匹配并赋值的参数个数。如果返回 0,说明一个都没匹配上。返回 EOF 表示输入结束或读取错误。
举个例子:
const char *data = "温度:25.3℃,湿度:68%";
float temp;
int humidity;
int matched = sscanf(data, "温度:%f℃,湿度:%d%%", &temp, &humidity);
// matched 是 2
// temp = 25.3, humidity = 68
注意这里的 %%——在格式字符串里,两个百分号表示匹配一个百分号字符。
🔑 核心要点:sscanf 的返回值一定要检查。我在项目中遇到过解析配置文件时,因为格式不匹配导致变量未被赋值,程序却继续使用未初始化数据的情况。那 bug 查得我头都大了。
格式说明符详解
这三个函数都使用相同的格式说明符。常用的有:
| 说明符 | 含义 | 示例 |
|---|---|---|
| %d | 有符号十进制整数 | sprintf(buf, "%d", -42); |
| %u | 无符号十进制整数 | sprintf(buf, "%u", 42u); |
| %x / %X | 十六进制(小写/大写) | sprintf(buf, "%x", 255); // "ff" |
| %f | 浮点数 | sprintf(buf, "%.2f", 3.14159); // "3.14" |
| %s | 字符串 | sprintf(buf, "%s", "hello"); |
| %c | 单个字符 | sprintf(buf, "%c", 'A'); |
| %p | 指针地址 | sprintf(buf, "%p", ptr); |
还有几个修饰符很重要:
%ld/%lu:long 类型%lld/%llu:long long 类型%zu:size_t 类型(这个我经常用)%.*s:指定长度的字符串(后面要跟两个参数:长度和指针)
实战:安全转换函数
基于上面的知识,我写了一个简单的安全转换工具。你想想看,实际项目中经常需要把数字转成字符串,但又不想每次都写一堆检查代码:
#include <stdio.h>
#include <stdbool.h>
// 安全地将整数转成字符串
bool int_to_str(int value, char *buf, size_t buf_size) {
int len = snprintf(buf, buf_size, "%d", value);
if (len < 0 || (size_t)len >= buf_size) {
// 转换失败或缓冲区不足
if (buf_size > 0) buf[0] = '\0';
return false;
}
return true;
}
// 安全地从字符串解析整数
bool str_to_int(const char *str, int *value) {
if (str == NULL || value == NULL) return false;
char dummy;
// 用 %n 检查是否消耗了所有字符
int matched = sscanf(str, "%d%c", value, &dummy);
return matched == 1; // 只有匹配到整数才算成功
}
💡 这里有个小技巧:sscanf 里用 %n 可以获取已读取的字符数。结合 %c 检查是否有多余字符,能更精确地验证输入格式。不过要注意,%n 不增加返回值计数。
常见陷阱与避坑指南
我总结几个容易踩的坑:
- 缓冲区大小计算错误:很多人给 snprintf 传
sizeof(buf)没问题,但传指针时就忘了。比如snprintf(ptr, sizeof(ptr), ...)——这里 sizeof(ptr) 是 4 或 8,不是缓冲区大小。 - 忽略返回值:不检查 snprintf 的返回值,数据被截断了也不知道。
- sscanf 的格式不匹配:比如用
%d去解析带小数点的字符串,匹配会失败。 - 忘记处理 % 本身:在格式字符串里,要输出 % 必须用
%%。 - 浮点数精度问题:
%f默认输出 6 位小数,但实际精度可能不够。用%.*f可以动态指定精度。
⚠️ 我曾经在一个日志系统里,用 sprintf 拼接时间戳和日志内容。缓冲区给了 256 字节,但日志内容偶尔会超长。结果就是日志缓冲区后面的内存被覆盖,导致其他模块的数据莫名其妙被改掉。那 bug 查了整整两天,最后发现是 sprintf 的锅。从那以后,我所有项目都强制要求用 snprintf。
知识体系图
下面这张图展示了字符串与数字转换的核心逻辑:
总结
说白了,字符串与数字转换就这么点事:
- 数字转字符串:用
snprintf,别用sprintf - 字符串转数字:用
sscanf,记得检查返回值 - 格式说明符:选对类型,注意精度
- 安全第一:缓冲区大小、返回值检查、边界条件,一个都不能少
嗯,这些函数虽然基础,但用好了能避免很多麻烦。我每次写代码都会默念一遍:snprintf 比 sprintf 安全,sscanf 返回值必须检查。习惯成自然,代码质量自然就上去了。
公众号:蓝海资料掘金营,微信deep3321