scanf 系列函数完整参考笔记
目录
函数族概览
标准函数
| 函数 | 功能 | 输入源 |
|---|---|---|
scanf() |
从标准输入读取 | stdin |
sscanf() |
从字符串读取 | const char *str |
fscanf() |
从文件流读取 | FILE *stream |
vscanf() |
可变参数版本 | stdin |
vsscanf() |
可变参数版本 | const char *str |
vfscanf() |
可变参数版本 | FILE *stream |
RT-Thread 封装
| 函数 | 对应标准函数 |
|---|---|
rt_sscanf() |
sscanf() |
rt_vsscanf() |
vsscanf() |
函数原型
int scanf(const char *format, ...);
int sscanf(const char *str, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int vscanf(const char *format, va_list ap);
int vsscanf(const char *str, const char *format, va_list ap);
int vfscanf(FILE *stream, const char *format, va_list ap);
返回值:成功匹配并赋值的输入项数量,失败返回 EOF 或小于期望值的数字。
格式说明符详解
基本格式:%[修饰符][宽度][长度]类型
类型说明符
整数类型
| 说明符 | 类型 | 示例输入 | 说明 |
|---|---|---|---|
%d |
int |
"123" |
十进制整数(可带正负号) |
%i |
int |
"123", "0xFF", "0755" |
自动识别进制(十进制/八进制/十六进制) |
%u |
unsigned int |
"123" |
无符号十进制整数 |
%o |
unsigned int |
"0755" |
八进制整数(可带或不带前导0) |
%x / %X |
unsigned int |
"FF", "0xFF" |
十六进制整数(大小写不敏感) |
示例:
int a, b;
unsigned int c;
sscanf("123 -456 0xFF", "%d %d %x", &a, &b, &c);
// a = 123, b = -456, c = 0xFF
浮点类型
| 说明符 | 类型 | 示例输入 | 说明 |
|---|---|---|---|
%f |
float |
"123.45" |
浮点数 |
%lf |
double |
"123.45" |
双精度浮点数 |
%e / %E |
float |
"1.23e2" |
科学计数法 |
%g / %G |
float |
"123.45" |
自动选择最短格式 |
示例:
float f;
double d;
sscanf("123.45 6.78e2", "%f %lf", &f, &d);
// f = 123.45, d = 678.0
字符和字符串
| 说明符 | 类型 | 示例输入 | 说明 |
|---|---|---|---|
%c |
char |
"A" |
单个字符(不跳过空白) |
%s |
char[] |
"Hello" |
字符串(遇到空白停止) |
%[字符集] |
char[] |
见下方 | 字符集匹配 |
示例:
char c, str[20];
sscanf("A Hello", "%c %s", &c, str);
// c = 'A', str = "Hello"
指针类型
| 说明符 | 类型 | 示例输入 | 说明 |
|---|---|---|---|
%p |
void* |
"0x12345678" |
指针地址 |
示例:
void *ptr;
sscanf("0x12345678", "%p", &ptr);
// ptr = (void*)0x12345678
特殊说明符
| 说明符 | 功能 | 说明 |
|---|---|---|
%n |
记录已读取字符数 | 不消耗输入,将已读取字符数写入 int* |
%% |
匹配字面量 % |
匹配一个 % 字符 |
示例:
int pos, value;
sscanf("12345", "%d%n", &value, &pos);
// value = 12345, pos = 5(已读取5个字符)
修饰符和宽度控制
宽度修饰符
| 格式 | 说明 | 示例 |
|---|---|---|
%5d |
最多读取5个字符 | "12345" → 读取 12345 |
%10s |
最多读取10个字符 | "HelloWorld" → 读取 HelloWorld |
%3c |
读取3个字符 | "ABC" → 读取 ABC |
注意:宽度限制可以防止缓冲区溢出。
示例:
char buf[10];
sscanf("1234567890123", "%9s", buf); // 安全:最多读取9个字符+'\0'
// buf = "123456789"
长度修饰符
| 修饰符 | 适用类型 | 说明 |
|---|---|---|
hh |
%hhd, %hhu |
char (signed/unsigned) |
h |
%hd, %hu |
short (signed/unsigned) |
l |
%ld, %lu, %lx |
long (signed/unsigned) |
ll |
%lld, %llu |
long long (signed/unsigned) |
L |
%Lf, %Le |
long double |
z |
%zd, %zu |
size_t |
t |
%td, %tu |
ptrdiff_t |
示例:
short s;
long l;
long long ll;
sscanf("123 456789 987654321", "%hd %ld %lld", &s, &l, &ll);
赋值抑制符 *
%* 表示读取但不赋值(跳过该字段)。
示例:
int second;
sscanf("123 456 789", "%*d %d %*d", &second);
// 跳过第一个和第三个数字,只读取第二个
// second = 456
实际应用(跳过前缀):
// 从 "+CWLAP:(3,"HONOR 20",-51,...)" 中提取数据
sscanf(line, "%*[^(](%d,\"%[^\"]\",%d,...)", &ecn, ssid, &rssi, ...);
// %*[^(] 跳过到 '(' 之前的所有字符
字符集匹配
字符集语法:%[字符集]
基本用法
| 格式 | 说明 | 示例 |
|---|---|---|
%[abc] |
匹配 a、b、c 中的任意字符 | "abc123" → "abc" |
%[a-z] |
匹配小写字母 | "hello123" → "hello" |
%[A-Z] |
匹配大写字母 | "HELLO123" → "HELLO" |
%[0-9] |
匹配数字 | "123abc" → "123" |
%[a-zA-Z0-9] |
匹配字母数字 | "Hello123!" → "Hello123" |
取反字符集:%[^字符集]
匹配不在字符集中的字符。
| 格式 | 说明 | 示例 |
|---|---|---|
%[^,] |
匹配到逗号之前的所有字符 | "Hello,World" → "Hello" |
%[^\"] |
匹配到引号之前的所有字符 | "Hello\"World" → "Hello" |
%[^\n] |
匹配到换行符之前的所有字符 | "Line1\nLine2" → "Line1" |
%[^0-9] |
匹配非数字字符 | "abc123" → "abc" |
特殊字符转义
在字符集中,某些字符需要特殊处理:
| 字符 | 处理方式 | 示例 |
|---|---|---|
] |
必须放在第一个位置 | %[]abc] 或 %[^]abc] |
- |
放在开头或结尾,或转义 | %[-abc] 或 %[abc-] |
^ |
放在非第一个位置表示字面量 | %[a^bc] |
实际应用示例(解析 AT 响应):
// 解析: +CIPDNS:0,"208.67.222.222","8.8.8.8"
char dns1[32], dns2[32];
sscanf(line, "%*[^\"]\"%[^\"]\",\"%[^\"]\"", dns1, dns2);
// %*[^\"] 跳过到第一个引号
// \" 匹配引号
// %[^\"] 读取引号内的内容到 dns1
// \" 匹配引号
// ,\" 匹配逗号和引号
// %[^\"] 读取引号内的内容到 dns2
// 解析: +CWLAP:(3,"HONOR 20",-51,"22:a6:f3:40:da:3d",1)
int ecn, rssi, channel;
char ssid[32], mac[32];
sscanf(line, "%*[^(](%d,\"%[^\"]\",%d,\"%[^\"]\",%d)",
&ecn, ssid, &rssi, mac, &channel);
// %*[^(] 跳过到 '(' 之前的所有字符
// ( 匹配左括号
// %d 读取加密方式
// ,\" 匹配逗号和引号
// %[^\"] 读取 SSID
// \" 匹配引号
// ,%d 读取 RSSI(可能是负数)
// ,\" 匹配逗号和引号
// %[^\"] 读取 MAC 地址
// \" 匹配引号
// ,%d 读取频道
// ) 匹配右括号
实际应用示例
示例1:解析 IP 地址和 MAC 地址
// 输入: "IP:192.168.1.100 MAC:00:11:22:33:44:55"
char ip_str[16];
unsigned char mac[6];
sscanf(input, "IP:%15[0-9.] MAC:%x:%x:%x:%x:%x:%x",
ip_str,
&mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]);
示例2:解析带引号的字符串
// 输入: Name="John Doe" Age=30
char name[32];
int age;
sscanf(input, "Name=\"%[^\"]\" Age=%d", name, &age);
// name = "John Doe", age = 30
示例3:跳过不需要的字段
// 输入: "Date:2024-01-15 Time:14:30:00"
int year, month, day, hour, min, sec;
sscanf(input, "Date:%d-%d-%d Time:%d:%d:%d",
&year, &month, &day, &hour, &min, &sec);
示例4:解析可变格式的数值
// 输入可能是: "123" 或 "0xFF" 或 "0755"
int value;
sscanf(input, "%i", &value); // %i 自动识别进制
示例5:解析固定宽度字段
// 输入: "20240115" (YYYYMMDD)
int year, month, day;
sscanf(input, "%4d%2d%2d", &year, &month, &day);
// year = 2024, month = 1, day = 15
示例6:解析带空格的字符串(使用字符集)
// 输入: "Hello World 123"
char text[32];
int num;
sscanf(input, "%[^0-9] %d", text, &num);
// text = "Hello World ", num = 123
// 注意:text 末尾可能有空格
示例7:记录解析位置
// 输入: "123 456 789"
int a, b, pos1, pos2;
sscanf(input, "%d%n %d%n", &a, &pos1, &b, &pos2);
// a = 123, pos1 = 3 (第一个数字结束位置)
// b = 456, pos2 = 7 (第二个数字结束位置)
示例8:解析浮点数数组
// 输入: "1.5,2.3,3.7,4.9"
float values[4];
int count = 0;
const char *p = input;
for (int i = 0; i < 4; i++) {
int n;
if (sscanf(p, "%f%n", &values[i], &n) == 1) {
count++;
p += n;
if (*p == ',') p++; // 跳过逗号
} else {
break;
}
}
常见问题和注意事项
1. 缓冲区溢出
错误示例:
char buf[10];
sscanf("This is a very long string", "%s", buf); // 危险!
正确做法:
char buf[10];
sscanf("This is a very long string", "%9s", buf); // 安全:限制宽度
2. 字符串读取遇到空格停止
%s 会在遇到空白字符(空格、制表符、换行等)时停止。
char str[32];
sscanf("Hello World", "%s", str);
// str = "Hello"(不是 "Hello World")
解决方案:使用字符集匹配
sscanf("Hello World", "%[^\n]", str); // 读取到换行符
// 或
sscanf("Hello World", "%31[^\n]", str); // 带宽度限制
3. 字符读取不跳过空白
%c 会读取任何字符,包括空白字符。
char c1, c2;
sscanf("A B", "%c %c", &c1, &c2);
// c1 = 'A', c2 = 'B'(空格被格式字符串中的空格跳过)
sscanf("AB", "%c%c", &c1, &c2);
// c1 = 'A', c2 = 'B'
4. 返回值检查
重要:始终检查返回值!
int a, b;
int ret = sscanf("123", "%d %d", &a, &b);
if (ret == 2) {
// 成功读取两个值
} else if (ret == 1) {
// 只读取了第一个值
} else {
// 读取失败
}
5. 格式字符串中的空白字符
格式字符串中的空白字符(空格、制表符、换行)会匹配输入中的任意数量的空白字符。
int a, b;
sscanf("123 456", "%d %d", &a, &b); // 多个空格也能匹配
sscanf("123\n456", "%d %d", &a, &b); // 换行也能匹配
6. 字面量匹配
格式字符串中非 % 开头的字符必须与输入完全匹配。
int value;
sscanf("Value:123", "Value:%d", &value); // 成功
sscanf("value:123", "Value:%d", &value); // 失败(大小写不匹配)
7. 十六进制和八进制前缀
%x/%X:可以识别0x或0X前缀,也可以没有前缀%o:可以识别0前缀,也可以没有前缀%i:自动识别0x(十六进制)、0(八进制)或无前缀(十进制)
unsigned int val;
sscanf("FF", "%x", &val); // val = 0xFF
sscanf("0xFF", "%x", &val); // val = 0xFF
sscanf("0755", "%o", &val); // val = 0755 (八进制)
sscanf("755", "%o", &val); // val = 0755 (八进制)
8. 负数处理
%d:可以读取负数%u:不能读取负数(会出错或产生意外结果)%x/%o:通常用于无符号数
int a;
unsigned int b;
sscanf("-123", "%d", &a); // a = -123
sscanf("-123", "%u", &b); // 错误:未定义行为
9. 浮点数精度
%f 读取 float,%lf 读取 double。
float f;
double d;
sscanf("123.45", "%f", &f); // 正确
sscanf("123.45", "%lf", &d); // 正确
sscanf("123.45", "%f", &d); // 错误:可能导致精度丢失
10. 字符集匹配的边界情况
char buf[32];
// 空字符集:匹配失败
sscanf("abc", "%[]", buf); // 返回 0
// 取反空字符集:匹配所有字符
sscanf("abc", "%[^]", buf); // buf = "abc"
// 包含 ']' 的字符集
sscanf("a]b", "%[]abc]", buf); // buf = "a]b"(如果 ']' 在开头)
调试技巧
1. 打印解析结果
int ret = sscanf(input, format, ...);
printf("Parsed %d items\n", ret);
2. 逐步解析
const char *p = input;
int pos;
int value;
while (sscanf(p, "%d%n", &value, &pos) == 1) {
printf("Found: %d at position %d\n", value, (int)(p - input));
p += pos;
// 跳过分隔符
while (*p == ' ' || *p == ',') p++;
}
3. 验证格式字符串
使用简单的测试用例验证格式字符串:
// 测试用例
const char *test_cases[] = {
"+CWLAP:(3,\"SSID\",-50,\"aa:bb:cc:dd:ee:ff\",1)",
"+CWLAP:(0,\"Test\",-80,\"11:22:33:44:55:66\",6)",
NULL
};
for (int i = 0; test_cases[i]; i++) {
int ecn, rssi, channel;
char ssid[32], mac[32];
int ret = sscanf(test_cases[i],
"%*[^(](%d,\"%[^\"]\",%d,\"%[^\"]\",%d)",
&ecn, ssid, &rssi, mac, &channel);
if (ret == 5) {
printf("OK: ecn=%d, ssid=%s, rssi=%d, mac=%s, channel=%d\n",
ecn, ssid, rssi, mac, channel);
} else {
printf("FAIL: parsed %d items\n", ret);
}
}
快速参考表
常用格式字符串模式
| 需求 | 格式字符串 | 示例输入 | 输出 |
|---|---|---|---|
| 跳过到某个字符 | %*[^X] |
"Prefix:123" |
跳过到 : |
| 读取到某个字符 | %[^X] |
"Hello,World" |
"Hello" |
| 读取引号内容 | \"%[^\"]\" |
"Name=\"John\"" |
"John" |
| 读取括号内容 | (%[^)]) |
"(123)" |
"123" |
| 读取固定宽度 | %4d |
"12345" |
1234 |
| 跳过字段 | %*d |
"123 456" |
跳过 123 |
| 读取十六进制 | %x |
"FF" |
255 |
| 读取 MAC 地址 | %x:%x:%x:%x:%x:%x |
"AA:BB:CC:DD:EE:FF" |
6个字节 |
格式化字符串解析
对于响应行:+CIPDNS:0,“208.67.222.222”,“8.8.8.8”
格式化字符串:“%*[^"]"%[^"]","%[^"]"”
分解说明:
| 格式 | 含义 | 匹配内容 |
|---|---|---|
| %*[^"] | 跳过非引号字符(* 表示不保存) | +CIPDNS:0, |
| " | 匹配一个引号 | " |
| %[^"] | 读取到引号前的所有字符(保存到 dns_server1) | 208.67.222.222 |
| " | 匹配引号 | " |
| ," | 匹配逗号和引号 | ," |
| %[^"] | 读取到引号前的所有字符(保存到 dns_server2) | 8.8.8.8 |
| " | 匹配最后一个引号 | " |
示例匹配过程
响应行: +CIPDNS:0,"208.67.222.222","8.8.8.8"
↓
格式串: %*[^"]" %[^"] "," %[^"] "
↓ ↓ ↓
跳过 提取 提取
0, dns_server1 dns_server2
关键点
-
%[^"]: 表示丢弃匹配内容,[^"] 表示匹配除引号外的任意字符
-
%[^"]:提取引号内的内容(IP 地址)
-
vsscanf:使用可变参数列表,支持多个输出参数
最后更新:2025-01-XX
适用标准:C99/C11