第26问:字符串与数字转换——sprintf与snprintf、sscanf

字符串和数字之间的转换,是C语言开发中最常见的操作之一。我做了这么多年嵌入式开发,几乎每个项目都会遇到这类需求。说白了,就是把数字变成人能读懂的字符串,或者反过来从字符串里把数字提取出来。

今天咱们就聊聊 sprintfsnprintfsscanf 这三个函数。它们看似简单,但坑不少。我见过太多同事在这上面栽跟头了。

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:安全的选择

snprintfsprintf 的安全版本。它多了一个参数——缓冲区大小:

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:反向解析

sscanfsprintf 的逆操作——从字符串里按格式提取数据:

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 不增加返回值计数。

常见陷阱与避坑指南

我总结几个容易踩的坑:

  1. 缓冲区大小计算错误:很多人给 snprintf 传 sizeof(buf) 没问题,但传指针时就忘了。比如 snprintf(ptr, sizeof(ptr), ...)——这里 sizeof(ptr) 是 4 或 8,不是缓冲区大小。
  2. 忽略返回值:不检查 snprintf 的返回值,数据被截断了也不知道。
  3. sscanf 的格式不匹配:比如用 %d 去解析带小数点的字符串,匹配会失败。
  4. 忘记处理 % 本身:在格式字符串里,要输出 % 必须用 %%
  5. 浮点数精度问题%f 默认输出 6 位小数,但实际精度可能不够。用 %.*f 可以动态指定精度。

⚠️ 我曾经在一个日志系统里,用 sprintf 拼接时间戳和日志内容。缓冲区给了 256 字节,但日志内容偶尔会超长。结果就是日志缓冲区后面的内存被覆盖,导致其他模块的数据莫名其妙被改掉。那 bug 查了整整两天,最后发现是 sprintf 的锅。从那以后,我所有项目都强制要求用 snprintf。

知识体系图

下面这张图展示了字符串与数字转换的核心逻辑:

字符串与数字转换核心逻辑 格式化转换 数字 → 字符串 sprintf / snprintf 字符串 → 数字 sscanf 整数:%d %u %x %ld %lld 浮点:%f %e %g %.2f 精度控制 格式匹配 返回值检查 边界处理 %n 检查 ⚠️ 安全第一:优先使用 snprintf,检查返回值

总结

说白了,字符串与数字转换就这么点事:

  • 数字转字符串:用 snprintf,别用 sprintf
  • 字符串转数字:用 sscanf,记得检查返回值
  • 格式说明符:选对类型,注意精度
  • 安全第一:缓冲区大小、返回值检查、边界条件,一个都不能少

嗯,这些函数虽然基础,但用好了能避免很多麻烦。我每次写代码都会默念一遍:snprintf 比 sprintf 安全,sscanf 返回值必须检查。习惯成自然,代码质量自然就上去了。


公众号:蓝海资料掘金营,微信deep3321