Lesson 46: sscanf 高级格式解析
练习任务
难度: 易-中
用 sscanf 实现 parse_addr() 函数,一行代码解析 "IP:Port" 格式字符串,不借助任何正则库:
int parse_addr(const char *input, char *host, int *port);
// 成功解析返回 1(host 和 port 通过参数传出),失败返回 0核心格式符:%[^:] —— 匹配"除冒号外的所有字符",实现轻量级字段提取。
验证用例:
输入 "192.168.1.100:8080\n" -> host: 192.168.1.100 port: 8080
输入 "localhost:3000\n" -> host: localhost port: 3000
输入 "invalid\n" -> parse error提示: 格式串
"%[^:]:%d"解析"host:port"——%[^:]读 host 至冒号,:匹配并消费字面冒号,%d读端口整数。sscanf返回值 = 成功匹配的字段数——n == 2说明两个字段都匹配成功,否则格式错误。
核心知识点
- 扫描集
%[^X]+^取反: 匹配到字符 X 前的所有字符,不消费 X(X 留给下一个格式符) - 返回值 = 成功赋值字段数: 这是判断格式有效性的金标准——不要用
!= EOF - 格式中字面量必须精确匹配且被消费:
"%d:%d"要求输入中有字面冒号: %svs%[^:]:%s遇任意空白停止,%[^:]仅遇:停止——处理"hello world:80"有显著差异- 宽度限制
%63[^:]防溢出: 对扫描集的 field-width 和对%s一样重要 - 赋值抑制
%*d/%*[^:]: 跳过字段但不存值——跳过不需要的部分 - sscanf 线程安全: 无内部静态状态、不修改输入字符串——对比
strtok的非线程安全 - 字段提取 vs 手写
strchr遍历取舍: 固定格式用sscanf(一行搞定),灵活解析用手写(状态机)
代码框架
#include <stdio.h>
#include <string.h>
/* 用 sscanf 解析 host:port 格式
* 成功返回 1(host 和 port 通过参数传出),失败返回 0 */
int parse_addr(const char *input, char *host, int *port) {
// 在这里实现 parse_addr:
// 1. int n = sscanf(input, "%[^:]:%d", host, port);
// 2. return (n == 2) ? 1 : 0;
}
int main(void) {
char line[256];
fgets(line, sizeof(line), stdin);
int len = strlen(line);
if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';
char host[128];
int port;
// 在这里实现 main:
// 1. if (parse_addr(line, host, &port))
// printf("host: %s\nport: %d\n", host, port);
// 2. else
// printf("parse error\n");
return 0;
}阅读框架后思考:
"%[^:]:%d"三个部分各做什么?- 为什么
port参数传的是&port(取地址)? - 如果输入是
"hello world:80",host 会拿到什么?
TIP
先不要往下翻看参考解答。在纸上拆解 "%[^:]:%d" 三个格式符的工作流程:%[^:] 扫描 host 直到 :、字面 : 消费冒号、%d 读端口号。试试用 %s 替代 %[^:] 会发生什么(思考 "hello world:80")。
深度讲解
1. 扫描集 %[^X] —— 核心武器
扫描集 (scanset) 是 sscanf 最强大的功能——它允许你定义一个字符集合,sscanf 会持续读入字符,直到遇到不在集合中的字符为止。
1.1 基本语法
%[charset] // 只读取 charset 中出现的字符
%[^charset] // 读取除 charset 外的所有字符(^ 取反)关键规则: ^ 必须放在方括号内的首位才表示取反,否则就是普通字符 ^。
1.2 常用扫描集速查
| 格式 | 含义 | 示例输入 | 匹配结果 |
|---|---|---|---|
%[^:] | 除冒号外所有字符 | "abc:123" | "abc" |
%[^\n] | 除换行外所有字符(读整行) | "hello\n" | "hello" |
%[^,] | 除逗号外所有字符 | "a,b,c" | "a" |
%[^=] | 除等号外所有字符 | "key=val" | "key" |
%[0-9] | 仅数字 | "123abc" | "123" |
%[a-zA-Z] | 仅字母 | "Hello123" | "Hello" |
%[^;] | 除分号外所有字符 | "x;y;z" | "x" |
%[^ ] | 除空格外所有字符 | "hello world" | "hello" |
1.3 停止但不消费
扫描集遇到停止字符时的行为非常优雅:停止,但不消费。
输入: "192.168.1.100:8080"
格式: "%[^:]:%d"
步骤 1: %[^:] 扫描
逐个读取 '1','9','2','.','1','6','8','.','1','.','1','0','0'
遇到 ':' → 停止! host = "192.168.1.100"
● 冒号未被消费,它在输入流中的位置不变
步骤 2: 字面 ':' 匹配
当前字符是 ':' → 匹配成功! 消费 ':'
● 此时才能前进到端口号
步骤 3: %d 扫描
读取 '8','0','8','0' → port = 8080
n = 2 → 解析成功 ✓TIP
"停止但不消费"这个行为是设计亮点——你可以用 %[^:] 读字段,然后用字面 : 消费分隔符,下一个格式符自然地从字段内容开始。这就像"我只读到分界线,但不把分界线拿走"。
1.4 不用 ^ 的正向扫描集
%[...] 也可以不取反——只匹配集合中的字符:
char buf[32];
sscanf("123abc", "%[0-9]", buf); // buf = "123"
sscanf("Hello123", "%[a-zA-Z]", buf); // buf = "Hello"
sscanf("abc123", "%[abc]", buf); // buf = "abc" (只匹配 a,b,c)WARNING
正向扫描集 %[charset] 要求输入至少有一个字符在集合内,否则匹配失败(返回 0)。这与 %[^charset] 不同——后者几乎总能匹配到一些字符,除非输入为空或第一个字符就是被排除的字符。
2. 返回值——格式有效性的金标准
sscanf 的返回值是成功赋值的字段数,不是读取的字符数,也不是 EOF。
2.1 逐级返回调含义
int sscanf(const char *str, const char *format, ...);
// 返回值 = 成功匹配置赋值的字段数
// 如果匹配置败或输入不匹配: 返回 0
// 如果遇到输入结束: 返回 EOF(通常为 -1)int a, b, c;
int n;
// 全部匹配
n = sscanf("10:20:30", "%d:%d:%d", &a, &b, &c);
// n = 3, a=10, b=20, c=30
// 部分匹配
n = sscanf("hello:80", "%d:%d", &a, &b);
// n = 0 ← %d 遇到 'h' 立即失败,a 和 b 未赋值!
// 只有 host 匹配
n = sscanf("invalid", "%[^:]:%d", buf, &a);
// n = 1 ← %[^:] 匹配了 "invalid",但 ':' 匹配失败,a 未赋值
// 输入为空字符串
n = sscanf("", "%d", &a);
// n = EOF (或 -1) ← 输入在第一个格式符前就结束了2.2 正确的防御性检查
// ❌ 错误:用 != EOF 判断成功率
int n = sscanf(input, "%[^:]:%d", host, &port);
if (n != EOF) { // 即使只匹配 1 个字段也不等于 EOF!
printf("host: %s, port: %d\n", host, port); // 可能用未初始化的 port
}
// ✅ 正确:检查返回值是否等于期望字段数
int n = sscanf(input, "%[^:]:%d", host, &port);
if (n == 2) {
printf("host: %s, port: %d\n", host, port); // 两个字段都拿到了
} else {
printf("parse error\n"); // 安全失败
}IMPORTANT
返回值检查是防御性编程的第一道防线。用 n == expected_count 而非 n != EOF——前者是"我拿到了所有字段"的确切保证,后者只能告诉你"没到输入末尾"。
2.3 与 scanf/fscanf 的对比
// scanf 返回匹配数,但不是所有场景都可信
int n = scanf("%d", &x); // 如果输入不匹配,返回 0
// sscanf 同样返回匹配数
int n = sscanf(str, "%d", &x); // 一致
// fscanf 从文件读,返回值含义相同
int n = fscanf(fp, "%d", &x);三者返回值的语义完全相同——成功赋值的字段数。所以学会 sscanf 的返回值用法,scanf/fscanf 也自然掌握了。
3. 格式中字面量必须精确匹配
格式串中的普通字符(非 % 引导的格式符)是字面匹配要求——它们必须在输入中精确出现,且会被消费。
3.1 字面量匹配示例
int a, b;
// 格式 "%d:%d" 要求输入中有字面冒号
n = sscanf("10:20", "%d:%d", &a, &b);
// n = 2, a = 10, b = 20 ✓ 冒号被匹配并消费
n = sscanf("10 20", "%d:%d", &a, &b);
// n = 1, a = 10, b 未赋值 ✗ 空格无法匹配 ':'
n = sscanf("10: 20", "%d:%d", &a, &b);
// n = 2, a = 10, b = 20 ✓ %d 会自动跳过前导空白NOTE
格式中的空白字符(空格、\t、\n)含义特殊——它不是字面匹配一个空白,而是跳过零个或多个空白。所以 "%d %d" 和 "%d\n%d" 在匹配行为上完全等价。
3.2 多分隔符场景
// 日期格式 "YYYY-MM-DD"
int y, m, d;
sscanf("2024-01-15", "%d-%d-%d", &y, &m, &d);
// y=2024, m=1, d=15
// 只要分隔符一致即可
sscanf("2024/01/15", "%d/%d/%d", &y, &m, &d);
// y=2024, m=1, d=15
// 混合分隔符
sscanf("2024-01/15", "%d-%d/%d", &y, &m, &d);
// y=2024, m=1, d=153.3 字面量不匹配 = 停止消费
输入: "10 20"
格式: "%d:%d"
扫描过程:
① %d → 读 "10", 到 ' ' 停止。a = 10
② 字面 ':' → 当前是 ' ' → 不匹配! → 整个 sscanf 停止
b 未被赋值
n = 1CAUTION
字面量匹配失败后,sscanf 立即停止——后续的格式符(包括 %d)不再执行。你不能依赖"后面的字段不会被赋值所以应该有自己的默认值"——未赋值的字段是未定义的。
4. %s vs %[^:] —— 根本性差异
这是实际开发中最容易踩的坑之一。
4.1 %s:遇任意空白停止
char word[64];
sscanf("hello", "%s", word); // word = "hello"
sscanf("hello world", "%s", word); // word = "hello" ← 遇到空格停了!
sscanf("hello:world", "%s", word); // word = "hello" ← 遇到 : 停了!%s 的停止条件:空格、\t、\n、\r、\v、\f……几乎所有空白字符。
4.2 %[^:]:仅遇 : 停止
char part[64];
sscanf("hello", "%[^:]", part); // part = "hello"
sscanf("hello world", "%[^:]", part); // part = "hello world" ← 空格不停止!
sscanf("hello:world", "%[^:]", part); // part = "hello" ← 遇到 : 停了4.3 关键对比:"hello world:80"
总是最经典的对照实验:
char host_s[64];
int port_s;
// 用 %s
int n1 = sscanf("hello world:80", "%s:%d", host_s, &port_s);
// 扫描过程:
// %s 读 "hello" → 遇到空格停止 → host_s = "hello"
// 字面 ':' → 当前是空格 ' ' → 不匹配! 失败
// n1 = 1 ← 只有 host 匹配成功
char host_bracket[64];
int port_bracket;
// 用 %[^:]
int n2 = sscanf("hello world:80", "%[^:]:%d", host_bracket, &port_bracket);
// 扫描过程:
// %[^:] 读 "hello world" → 遇到 ':' 停止 → host_bracket = "hello world"
// 字面 ':' → 当前是 ':' → 匹配!
// %d 读 "80" → port_bracket = 80
// n2 = 2 ← 全部匹配成功!┌─────────────┬──────────────────┬─────────────────────┐
│ 格式 │ 输入 │ 结果 │
├─────────────┼──────────────────┼─────────────────────┤
│ "%s:%d" │ "hello world:80" │ host="hello", n=1 │
│ "%[^:]:%d" │ "hello world:80" │ host="hello world" │
│ │ │ port=80, n=2 │
└─────────────┴──────────────────┴─────────────────────┘4.4 选型指南
| 场景 | 推荐 | 原因 |
|---|---|---|
| 字段不含空白 | %s | 最简单 |
| IP 地址解析 | 两者都可以(IP 不含空白和冒号) | 效果等效 |
| 域名解析 | %[^:] | 域名不含冒号,但可能出现在含空白的文本中 |
| 含空格的值 | %[^:] | %s 会截断 |
| CSV 解析 | %[^,] | 逗号是分隔符 |
| 任意单行文本 | %[^\n] | 读到行尾 |
5. 宽度限制 %63[^:] —— 防溢出的安全带
扫描集在读入时不知道目标缓冲区有多大——你必须在格式串中设置上限。
5.1 不带宽度的危险
char host[16]; // 只有 16 字节的缓冲区
// ❌ 危险:没有宽度限制
sscanf("very-long-hostname.example.com:8080", "%[^:]:%d", host, &port);
// host 缓冲区只有 16 字节,但 %[^:] 写了远超 16 字节的内容
// → 栈溢出 (stack buffer overflow)!5.2 加宽度限制
char host[16];
// ✅ 安全:最多读 15 个字符,为 '\0' 留 1 字节
sscanf("very-long-hostname.example.com:8080", "%15[^:]:%d", host, &port);
// host = "very-long-hostn" (截断),port = 8080
// 缓冲区安全 ✓5.3 宽度限制规则
#define HOST_MAX 64
char host[HOST_MAX]; // 64 字节
int port;
// 宽度 = 缓冲区大小 - 1(为 '\0' 预留)
sscanf(input, "%63[^:]:%d", host, &port);
// ^^ — 63 = 64 - 1WARNING
扫描集的宽度限制规则和 %s 完全一致:%Ns / %N[...] 最多读 N 个字符,自动追加 \0。N 必须是缓冲区大小减 1。忘加宽度限制是 C 中最常见的缓冲区溢出来源之一。
5.4 宽度限制对扫描集的影响
输入: "aaaaaaaaaab:123" (12 个 'a' 后跟 ":123")
host 缓冲区: host[8] (8 字节)
不加限制: %[^:] → 读到 12 个 'a' → 写入 13 字节 (12 + '\0')
→ 缓冲区溢出!✗
加限制: %7[^:] → 最多读 7 个 'a' → host = "aaaaaaa"
→ 剩余输入位置: "aaab:123" 之后的部分
→ ':' 被字面匹配消费,%d 读取 123
→ 安全 ✓6. 赋值抑制 %* —— 跳过不关心的字段
赋值抑制(assignment suppression)用星号 * 标记——匹配但不存储。这在"只关心部分字段"的场景中非常有用。
"user:123:admin"
user ↑ ↑
关心 跳过 关心6.1 基本语法
%*d // 匹配整数组不赋值(吃掉这个字段)
%*s // 匹配字符串但不赋值
%*[^:] // 匹配到冒号但不保存
%*[^,] // 匹配到逗号但不保存6.2 实战示例
char username[32];
char role[32];
// 输入 "user:123:admin" —— 跳过中间的数字
int n = sscanf("user:123:admin", "%[^:]:%*d:%[^:]", username, role);
// n = 2 ← %*d 不返回值
// username = "user"
// role = "admin"
// 中间的 123 被跳过6.3 多个字段跳过
// 日志 "2024-01-15 14:30:00 INFO user login" → 只提取时间和消息
char date[16], time[16], message[64];
sscanf(log, "%s %s:%*d:%*d %*s %[^\n]", date, time, message);
// date = "2024-01-15"
// time = "14"
// 跳过了 :30 :00 和 "INFO" 级别
// message = "user login"NOTE
赋值域制不改变 sscanf 的返回值——%*d 匹配了整数但不赋值,所以它不计入返回的字段数。上面例子中 n = 2(只有 username 和 role 被赋值)。
6.4 常见用例
| 场景 | 格式 | 效果 |
|---|---|---|
| 跳中间字段 | "%[^:]:%*d:%[^:]" | 提取首尾字段 |
| 跳前缀 | "%*[^=]=%s" | 只取等号后的值 |
| 跳后缀 | "%s%*[^\n]" | 只取第一个词 |
| 跳标记 | "%*[#]%s" | 跳过注释符 |
7. sscanf 线程安全 —— 与 strtok 的关键区别
7.1 sscanf 是完全线程安全的
// sscanf 的内部结构(简化)
int sscanf(const char *str, const char *format, ...) {
// str 是 const — 只读不写
// 无内部静态变量
// 无全局状态
// 每次调用完全独立
return internal_scan(str, format, va_args);
}sscanf 不修改输入字符串(参数是 const char *),没有全部静态状态,每次调用完全独立。多个线程可以同时调用 sscanf 而不会互相干扰。
7.2 strtok 是非线程安全的
// strtok 的内部(简化)
char *strtok(char *str, const char *delim) {
static char *saved; // ← 静态变量! 多线程共享!
if (str) saved = str;
return strtok_r(saved, delim, &saved); // 实际这个版本才有 saveptr
}strtok 使用静态变量保存分词位置——两个线程同时调用会导致状态互相覆盖。相关课程详见 Lesson 43: strtok_r。
7.3 完整对比表
| 维度 | sscanf | strtok (不安全) | strtok_r (安全) |
|---|---|---|---|
| 修改输入 | 不修改 (const) | 修改(写入 \0) | 修改(写入 \0) |
| 线程安全 | 是 | 否(静态变量) | 是(调用者提供状态) |
| 内部状态 | 无 | 有(static 指针) | 有(saveptr 参数) |
| 重入性 | 天然可重入 | 不可重入 | 可重入 |
| 适用场景 | 固定格式一次提取 | 逐 token 切分 | 逐 token 切分 |
IMPORTANT
如果你在多线程程序中需要字符串切分,有两个安全选择:sscanf(固定格式,一次提取所有字段)或 strtok_r(逐 token 切分,调用者提供状态)。
8. 字段提取 vs 手写 strchr 遍历 —— 何时选哪个
这是工程中的常见抉择——同样的功能可以用 sscanf 一行搞定义也可以手写字符遍历。
8.1 用 sscanf 的场景
固定格式、分隔符明确:
// 解析 IP:Port
sscanf(input, "%[^:]:%d", host, &port); // 1 行
// 解析日期 YYYY-MM-DD
sscanf(date, "%d-%d-%d", &y, &m, &d); // 1 行
// 解析 key=value
sscanf(line, "%[^=]=%s", key, value); // 1 行
// 解析 CSV
sscanf(row, "%[^,],%[^,],%d", name, email, &age); // 1 行8.2 用手写遍历的场景
格式灵活、需要状态机:
// 引号内有分隔符的 CSV: "Smith, John",42,"NY"
// sscanf 处理不了 —— 引号内的逗号不应该被当作分隔符
// 多层嵌套的配置: section: { key1 = val1; key2 = val2 }
// sscanf 无法处理嵌套结构
// 需要跳过注释/空白行
// sscanf 需要配合外部逻辑,不如手写直观
// 需要精确控制错误位置和错误消息
// sscanf 只告诉你 "匹配了几个字段"8.3 手写 strchr 等价实现
// sscanf 版本: 1 行
int parse_sscanf(const char *input, char *host, int *port) {
return sscanf(input, "%[^:]:%d", host, port) == 2;
}
// 手写 strchr 版本: ~8 行
int parse_manual(const char *input, char *host, int *port) {
const char *colon = strchr(input, ':');
if (!colon) return 0; // 没有冒号
size_t len = colon - input;
if (len >= 128) return 0; // host 太长
memcpy(host, input, len); // 复制 host
host[len] = '\0'; // 补 '\0'
char *end;
long p = strtol(colon + 1, &end, 10); // 解析端口
if (end == colon + 1) return 0; // 端口不同数字
*port = (int)p;
return 1;
}8.4 决策对比
| 维度 | sscanf | 手写 strchr |
|---|---|---|
| 代码行数 | 1-3 行 | 8-15 行 |
| 可读性 | 格式串即文档 | 需逐步阅读 |
| 错误定位 | 只能知道匹配了几个字段 | 精确知道哪里出错 |
| 性能 | 库优化,通常更快 | 相当 |
| 灵活性 | 仅限固定格式 | 任意复杂解析 |
| 溢出保护 | 需要宽度限制 | 需要手动检查长度 |
| 学习曲线 | 需记住格式符 | 通用 C 知识 |
8.5 黄金法则
能用 sscanf 一行搞定的,就用 sscanf。
重要状态机、嵌套结构、或精确错误信息的,用手写遍历。
两者结合也是好策略——sscanf 提取固定义分,手写处理灵活部分。NOTE
在性能敏感的路径上,两者差异很小(sscanf 内部也是字符遍历)。真正的性能差异来自算法选择,而不是 sscanf vs 手写。
参考解答
parse_addr 实现
#include <stdio.h>
#include <string.h>
int parse_addr(const char *input, char *host, int *port) {
/* 一行 sscanf 解析 host:port
*
* %[^:] — 扫描集取反:匹配除冒号外的所有字符
* "192.168.1.100:8080" → host = "192.168.1.100"
* 读到 ':' 停止但不消费冒号
* : — 字面匹配:消费输入中的冒号
* %d — 整数:匹配端口号
*
* 返回值 = 成功赋值域字段数
* n == 2 → host 和 port 都拿到了 → 格式正确
* n < 2 → 格式不匹配 → 解析失败
*/
int n = sscanf(input, "%127[^:]:%d", host, port);
return (n == 2) ? 1 : 0;
}IMPORTANT
注意 %127[^:] 中的 127 是宽度限———host 缓冲区大小为 128,最多读 127 个字符,为 \0 留 1 字节。这是必须的安全措施。
main 实现
int main(void) {
char line[256];
fgets(line, sizeof(line), stdin);
int len = strlen(line);
if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';
char host[128];
int port;
if (parse_addr(line, host, &port)) {
printf("host: %s\nport: %d\n", host, port);
} else {
printf("parse error\n");
}
return 0;
}完整编译和测试
# 编译
gcc -Wall -Wextra -o 46_sscanf solution_46_sscanf.c
# 测试
echo "192.168.1.100:8080" | ./46_sscanf
# 输出:
# host: 192.168.1.100
# port: 8080
echo "localhost:3000" | ./46_sscanf
# 输出:
# host: localhost
# port: 3000
echo "invalid" | ./46_sscanf
# 输出:
# parse error课堂讨论
讨论问题
Q1: %s 和 %[^:] 的本质区别是什么?
%s 遇任意空白字符停止: 空格、\t、\n、\r 等),而 %[^:] 仅遇 : 停止。
这决定了它们适用的场景完全不同:
%s适合提取不含空白的单词%[^:]适合提取"直到冒号"的整段文字(可能包含空格)
经典差异示例:"hello world:80"
%s→"hello"(空格截断)%[^:]→"hello world"(直到冒号)
Q2: sscanf 和 strtok 各适合什么场景?
| 场景 | 选择 | 原因 |
|---|---|---|
| 固定格式(IP:Port, YYYY-MM-DD) | sscanf | 一行搞定,格式串即文档 |
| 按分隔符切分不定长 token 列表 | strtok/strtok_r | 逐个取 token 更自然 |
| 多线程环境 | sscanf 或 strtok_r | strtok 有静态变量不安全 |
| 需要保留开始字符串 | sscanf | strtok 会修改输入 |
| 字段间有复杂关系 | 手写状态机 | 两者都不适合 |
Q3: 为什么 %[^:] 中的 ^ 要放在方括号里面?
方括号 [...] 定义的是一种"字符集"(character set),这和正则表达式中的字符类 [...] 语法一致:
^在方括号内首位 → 取反(negation):匹配不在集合中的字符- 不在首位 → 普通字符
^:匹配字面上的^
%[^abc] // ^ = 取反,匹配除 a,b,c 外的字符
%[abc^] // ^ = 普通字符,匹配 a,b,c,^ 中的字符
%[a^bc] // ^ = 普通字符,匹配 a,^,b,c 中的字符NOTE
这和正则表达式中的 [^abc] 含义完全一致,只是 sscanf 的扫描集不是完整的正则引擎。
Q4: sscanf 能解析 CSV 吗?
能解析简单的 CSV,但不能处理复杂情况。
// 简单 CSV 行:字段不含引号、逗号、换行
sscanf(line, "%[^,],%[^,],%d", name, email, &age); // 可以不能处理的情况:
- 引号内的逗号:
"Smith, John",42——sscanf看到逗号就切 - 引号内的换行:跨行字段
- 转义引号:
"He said \"hello\""
对于复杂 CSV,重要手写状态机(逐字符处理,跟踪是否在引号内)。
Q5: 如何防止 %[^:] 导致的缓冲区溢出?
在格式中加宽度限制:
char host[32]; // 32 字节缓冲区
// ❌ 危险
sscanf(input, "%[^:]:%d", host, &port);
// ✅ 安全:最多读 31 个字符,为 '\0' 留 1 字节
sscanf(input, "%31[^:]:%d", host, &port);规则:%N[^chars] — N = 缓冲区大小 - 1。这和 snprintf 的 size 参数、fgets 的 size 参数遵循同样的"减一"原则。
课后练习
知识点提示: 每次写
sscanf后立即检查返回值是否等于期望的字段数。宽度限制是必须的安全措施。
练习 1: 解析日期格式
编写代码用 sscanf 解析日期字符串 "YYYY-MM-DD",提取年、月、日三个整数。如果格式不匹配,打印 "invalid date"。
输入示例: "2024-01-15" → Year: 2024, Month: 1, Day: 15
输入示例: "hello" → invalid date知识点提示: 格式串
"%d-%d-%d"中的-是字面量,必须精确匹配输入中的-。
参考解答
int y, m, d;
if (sscanf(input, "%d-%d-%d", &y, &m, &d) == 3) {
printf("Year: %d, Month: %d, Day: %d\n", y, m, d);
} else {
printf("invalid date\n");
}练习 2: 提取文件名和扩展名
编写代码解析文件名字符串 "document.txt",用扫描集分别提取文件名和扩展名。
输入示例: "document.txt" → name: document ext: txt
输入示例: "archive.tar.gz" → name: archive.tar ext: gz (注意只有最后一个点)
输入示例: "Makefile" → invalid filename知识点提示:
%[^.]匹配直到点号的字符,再用字面.消费点号,最后%[^.]提取扩展名。
参考解答
char name[128], ext[32];
if (sscanf(input, "%127[^.].%31[^.]", name, ext) == 2) {
printf("name: %s ext: %s\n", name, ext);
} else {
printf("invalid filename\n");
}NOTE
archive.tar.gz → name = archive.tar, ext = gz。如果需要先去除最后一个点号之前的所有字符,可以用 strrchr 配合 sscanf,或手写遍历。
练习 3: 跳过中间字段
编写代码解析 "user:123:admin",只提取用户名和角色,跳过中间的数字 ID。
输入: "user:123:admin" → user: user, role: admin
输入: "root:0:superuser" → user: root, role: superuser知识点提示: 用
%*d跳过中间的数字字段——匹配但不赋值。
参考解答
char user[64], role[64];
if (sscanf(input, "%63[^:]:%*d:%63[^:]", user, role) == 2) {
printf("user: %s, role: %s\n", user, role);
}
// %*d 匹配并跳过中间数字,不计入返回值练习 4: CSV 行解析
解析一行包含三个字段的简单 CSV:"John,john@mail.com,25"。
输入: "John,john@mail.com,25" → Name: John, Email: john@mail.com, Age: 25
输入: "Alice,alice@dev.io" → invalid CSV知识点提示: 三个
%[^,]用字面逗号分隔。第三个字段是整数,用%d而非%[^,]。
参考解答
char name[64], email[64];
int age;
if (sscanf(input, "%63[^,],%63[^,],%d", name, email, &age) == 3) {
printf("Name: %s, Email: %s, Age: %d\n", name, email, age);
} else {
printf("invalid CSV\n");
}练习 5: 带空格的配置行解析
解析可能的配置行 "key = value"(等号两边可能有空格),正确提取 key 和 value。
输入: "timeout = 30" → key: timeout, value: 30
输入: "host=192.168.1.1" → key: host, value: 192.168.1.1
输入: "=onlyvalue" → invalid config知识点提示: 格式中加空格可以跳过输入中的任意空白。
%[^= ]排除等号和空格,确保 key 不含尾部空格。
参考解答
char key[64], value[64];
// 格式 " %[^= ] = %s" 中的空格会跳过任意空白
if (sscanf(input, " %63[^= ] = %63s", key, value) == 2) {
printf("key: %s, value: %s\n", key, value);
} else {
printf("invalid config\n");
}
// %[^= ] 排除 '=' 和空格 → key = "timeout" (不含尾部空格)
// 格式中的空格跳过 '=' 两边的空白
// %s 读取 value参考资料
man 3 sscanf — 官方手册页,包含所有格式符和扫描集语法的完整说明
"The sscanf() function reads input from the character string str and interprets it according to format."
C 标准 §7.21.6.7 —
sscanf函数的规范定义,包括返回值语义和所有格式化选项Lesson 43: strtok_r — 线程安全切分 — 另一种字符串解析方式,与
sscanf互补:strtok_r适合逐 token 切分,sscanf适合固定格式一次提取Lesson 42: snprintf — 安全格式化输出 — 格式字符串的对称面:
snprintf是输出格式化,sscanf是输入解析,两者共用相同的格式符体系(%d,%s, 宽度限制等)man 3 scanf — 完整的格式符参考手册,包含
%,%[,%n等全部格式符的详细说明"The scanf() family of functions scans formatted input like sscanf(3), but read from a FILE or stdin."
Linux Programmer's Manual: scanf(3) — 扫描集
%[...]和%[^...]的官方语法说明,包含宽度限制和赋值抑制的组合用法"Matches a nonempty sequence of characters from the specified set of accepted characters; the next pointer must be a pointer to char."