第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
那有没有更安全的函数?有。strtol、strtoul、strtod就是来救场的。它们多了一个参数,能告诉你解析到哪里出错了。
#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
sscanf是scanf的字符串版本,从字符串里按格式解析数据。用法和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 知识体系总览
下面这张图总结了字符串转换的核心函数和它们之间的关系。你可以把它当作一个快速参考。
7.7 实战建议与避坑总结
说了这么多,我总结几条实战经验,你直接拿去用:
- 解析整数用
strtol,别用atoi。除非你100%确定输入合法,否则atoi的0返回值会让你抓狂。 - 格式化输出用
snprintf,别用sprintf。缓冲区溢出是C语言最经典的漏洞之一,别给自己挖坑。 - 浮点数转换要显式指定精度。默认
%f只保留6位小数,不够用就自己写%.15f。 - 检查
sscanf的返回值。返回值告诉你匹配了多少个字段,少一个都说明格式有问题。 - 用
endptr做错误定位。strtol和strtod的第二个参数不是摆设,它能帮你快速找到解析失败的位置。
🔑 一句话总结
字符串转换的核心就两件事:安全和精度。安全靠snprintf和strtol,精度靠显式指定格式。记住这两点,你就能避开90%的坑。
好了,这一章就讲到这里。这些函数看着简单,但用好了能省很多事。下次写代码的时候,多想想:我的输入可靠吗?我的缓冲区够大吗?我的精度够用吗?想清楚了再动手。
公众号:蓝海资料掘金营,微信deep3321