第7章 字符串转换:atoi、atol、atof、strtol、sprintf、sscanf 的用法与精度控制

字符串和数字之间的转换,是C语言里最基础也最容易踩坑的地方。我做了十几年嵌入式开发,见过太多因为转换函数用错导致的bug——有的直接死机,有的数据对不上,查半天才发现是精度丢了。今天咱们就把这几个函数掰开揉碎,讲清楚。

7.1 简单转换:atoi、atol、atof

这三个函数,名字就告诉你了:atoi是ASCII to Integer,atol是ASCII to Long,atof是ASCII to Float。用法很简单,传个字符串进去,返回数字。

#include <stdlib.h>
#include <stdio.h>

int main() {
    char str1[] = "123";
    char str2[] = "-456";
    char str3[] = "3.14159";
    
    int a = atoi(str1);
    long b = atol(str2);
    double c = atof(str3);
    
    printf("a = %d\n", a);   // 123
    printf("b = %ld\n", b);  // -456
    printf("c = %f\n", c);   // 3.141590
    
    return 0;
}

看着挺简单对吧?但坑就藏在简单里。

⚠️ 避坑指南:atoi/atol/atof 没有错误检测

我曾经在一个数据采集项目里用atoi解析传感器返回的字符串。有一次传感器坏了,返回了"ERR_123"。你猜怎么着?atoi返回了0,没有任何报错。我查了三天,才发现是解析出了问题。

这三个函数遇到非法字符时,直接返回0。你根本分不清是"0"这个字符串,还是解析失败。所以我的建议是:生产环境慎用atoi系列,除非你100%确定输入格式正确。

7.2 安全转换:strtol、strtoul、strtod

那有没有更安全的函数?有。strtolstrtoulstrtod就是来救场的。它们多了一个参数,能告诉你解析到哪里出错了。

#include <stdlib.h>
#include <stdio.h>

int main() {
    char str[] = "  123abc";
    char *endptr;
    
    long val = strtol(str, &endptr, 10);
    
    if (*endptr != '\0') {
        printf("解析到非法字符: %s\n", endptr);
    } else {
        printf("解析成功: %ld\n", val);
    }
    
    // 输出: 解析到非法字符: abc
    return 0;
}

看到没?endptr指向了第一个无法解析的字符。如果*endptr == '\0',说明整个字符串都解析完了。这个机制,说白了就是给了你一个错误定位的能力。

第三个参数是进制数。10就是十进制,16就是十六进制,0的话会自动识别(0x开头当十六进制,0开头当八进制)。我个人习惯用0,省事。

💡 个人经验:strtol 的第二个参数

我建议你每次都传&endptr,哪怕你暂时不需要。为什么?因为一旦出问题,这个指针能帮你快速定位。我在一个通信协议解析模块里,就是用endptr来检查每一段数据是否合法,省去了很多调试时间。

7.3 格式化输出:sprintf 与 snprintf

sprintf是把各种类型的数据格式化输出到字符串里。用法和printf一样,只是输出目标变成了字符串。

#include <stdio.h>

int main() {
    char buf[100];
    int a = 42;
    double b = 3.1415926535;
    
    sprintf(buf, "a = %d, b = %.3f", a, b);
    printf("%s\n", buf);  // a = 42, b = 3.142
    
    return 0;
}

sprintf有个致命问题:缓冲区溢出。你想想看,如果目标字符串比buf大,会发生什么?直接写穿内存,轻则数据错乱,重则程序崩溃。

所以C99标准引入了snprintf。它多了一个参数,指定最大写入长度。

#include <stdio.h>

int main() {
    char buf[10];
    int a = 123456789;
    
    int len = snprintf(buf, sizeof(buf), "%d", a);
    
    printf("buf = %s\n", buf);   // 123456789(截断到9个字符+'\0')
    printf("实际需要长度: %d\n", len);  // 9
    
    return 0;
}

注意:snprintf返回的是如果空间足够,应该写入的字符数(不包括'\0')。如果返回值大于等于缓冲区大小,说明被截断了。这个机制可以用来动态计算所需缓冲区大小。

⚠️ 避坑指南:snprintf 的返回值

我曾经在日志系统里用snprintf拼接字符串,以为返回值就是实际写入的长度。结果有一次日志太长被截断了,我还在用返回值做后续处理,导致数据对不上。记住:返回值是"应该写入的长度",不是"实际写入的长度"。实际写入长度是min(返回值, 缓冲区大小-1)

7.4 格式化输入:sscanf

sscanfscanf的字符串版本,从字符串里按格式解析数据。用法和scanf一样,但输入源是字符串。

#include <stdio.h>

int main() {
    char str[] = "2024-03-15 14:30:00";
    int year, month, day, hour, minute, second;
    
    int ret = sscanf(str, "%d-%d-%d %d:%d:%d", 
                     &year, &month, &day, 
                     &hour, &minute, &second);
    
    if (ret == 6) {
        printf("解析成功: %d年%d月%d日 %d:%d:%d\n", 
               year, month, day, hour, minute, second);
    } else {
        printf("解析失败,成功匹配了%d个字段\n", ret);
    }
    
    return 0;
}

sscanf的返回值是成功匹配并赋值的字段数。如果返回值小于预期,说明格式不匹配。这个机制可以用来做简单的格式校验。

💡 个人经验:sscanf 的格式字符串

我建议你在格式字符串里加上宽度限制,比如%4d只匹配4位数字。为什么?因为如果输入是"20240315",没有宽度限制的话,%d会一口气把8位数字都吃掉,后面的字段就全乱了。加上宽度限制,每个字段只吃自己该吃的部分。

7.5 精度控制:浮点数转换的陷阱

浮点数转换是精度丢失的重灾区。你想想看,3.1415926535在计算机里存的是近似值,转成字符串再转回来,精度还能保证吗?

#include <stdio.h>
#include <stdlib.h>

int main() {
    double d = 3.141592653589793;
    char buf[50];
    
    // 默认精度
    sprintf(buf, "%f", d);
    printf("默认: %s\n", buf);  // 3.141593(只保留6位小数)
    
    // 指定精度
    sprintf(buf, "%.15f", d);
    printf("15位: %s\n", buf);  // 3.141592653589793
    
    // 再转回来
    double d2 = atof(buf);
    printf("差值: %e\n", d - d2);  // 0.000000e+00(理论上)
    
    return 0;
}

看到没?默认%f只保留6位小数,精度直接丢了。如果你需要高精度,必须显式指定小数位数。

🔑 精度控制要点

  • %f:默认6位小数,四舍五入
  • %.nf:指定n位小数
  • %g:自动选择%f或%e,去掉末尾的0
  • %e:科学计数法
  • %a:十六进制浮点数(C99,无精度损失)

7.6 知识体系总览

下面这张图总结了字符串转换的核心函数和它们之间的关系。你可以把它当作一个快速参考。

字符串转换函数体系 字符串 字符串 → 数字 atoi / atol / atof strtol / strtoul strtod / strtof 数字 → 字符串 sprintf / snprintf sscanf(反向解析) ⚠️ 核心注意事项 缓冲区溢出 | 精度丢失 | 错误检测 | 返回值检查

7.7 实战建议与避坑总结

说了这么多,我总结几条实战经验,你直接拿去用:

  1. 解析整数用strtol,别用atoi。除非你100%确定输入合法,否则atoi的0返回值会让你抓狂。
  2. 格式化输出用snprintf,别用sprintf。缓冲区溢出是C语言最经典的漏洞之一,别给自己挖坑。
  3. 浮点数转换要显式指定精度。默认%f只保留6位小数,不够用就自己写%.15f
  4. 检查sscanf的返回值。返回值告诉你匹配了多少个字段,少一个都说明格式有问题。
  5. endptr做错误定位strtolstrtod的第二个参数不是摆设,它能帮你快速找到解析失败的位置。

🔑 一句话总结

字符串转换的核心就两件事:安全精度。安全靠snprintfstrtol,精度靠显式指定格式。记住这两点,你就能避开90%的坑。

好了,这一章就讲到这里。这些函数看着简单,但用好了能省很多事。下次写代码的时候,多想想:我的输入可靠吗?我的缓冲区够大吗?我的精度够用吗?想清楚了再动手。


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