扫描集 & 字符集
基本概念
%[] 格式说明符是一种控制读取内容的工具,其专有名词是扫描集(scan set),目前的实践证明扫描集仅能运用在 scanf() 函数和 sscanf() 函数上。方括号[]中能够填入的内容就是字符集。
扫描集的工作原理是:如果输入的字符匹配方括号内字符集,那么就进行操作,每次会在已经提取的字符后面自动添加’\0’。方括号 [] 中字符集的内容基本上可以划分为三种:
- 指定:
%[xxx]
,这是最常规的扫描集,它会匹配到字符集中的任意一个或连续的字符,并将这些字符存储到指定的缓冲区中,一直到下一个字符不属于该字符集,匹配就会停止; - 排除:
%[^xxx]
,^ 代表取补集。它的作用和指定刚好相反,如果匹配到字符集中的任意一个字符,匹配将结束,在匹配结束之前所有的内容都会进入指定的缓冲区; - 跳过:
%*[xxx]
,其中 * 的名称是赋值抑制字符,用于告诉函数 sscanf() 不要将匹配到的结果存储在提供的变量中。它的作用和排除比较类似,但执行的是“匹配但不存储”。赋值抑制符不仅仅可以搭配扫描集,还可以搭配 %d、%s 等;它不仅可以搭配常规扫描集,还可以搭配补集,e.g.%*[^xxx]
。
基本运用
- 假设我们执行这一语句:
sscanf(result, "%[abc]", id);
,%[abc]
会匹配字符’a’
、’b’
或’c’
中的任意一个。- 如果字符串 result 为
"apple123"
,字符串 id 就会变成'a'
; - 如果字符串 result 为
"banana123"
,字符串 id 就会变成"ba"
,即便后续 result[3] 和 result[5] 都是 a,因为匹配进行到 result[2] 时为'n'
,匹配已经结束,所以后面两个 a 都不会被匹配; - 如果字符串 result 为
"abandon123"
,字符串id
就会变成"aba"
; - 如果字符串 result 为
"acclaim123"
,字符串 id 就会变成"acc"
。
- 如果字符串 result 为
- 假设我们执行这一语句:
sscanf(result, "%[^abc]", id);
,%[^abc]
只要遇到字符'a'
、'b'
或'c'
中的任意一个就不会匹配。- 如果字符串 result 为
"apple123"
,字符串 id 就会变成''
,即什么都没有; - 如果字符串 result 为
"flexible123"
,字符串 id 就会变成"flexi"
,即便后续字符串中没有字符 ‘a’、‘b’ 或 ‘c’ 中的任何一个了,因为匹配进行到 result[5] 时为'b'
,匹配已经结束,所以后面的字符都不会被匹配; - 如果字符串 result 为
"123apple"
,字符串 id 就会变成"123"
。
- 如果字符串 result 为
- 假设我们执行这一语句:
sscanf(result, "%*[abc]", id);
,%*[abc]
只要遇到字符'a'
、'b'
或'c'
中的任意一个就会跳过,匹配停止。同理,%*[^abc]
意味着只有遇到字符'a'
、'b'
或'c'
中的任意一个才会匹配,否则匹配停止。但是,使用了赋值抑制字符意味着匹配但不存储,即使匹配成功,也不会有任何字符存储到指定的缓冲区,因此扫描集须搭配其它格式符使用,e.g.sscanf(result, "%*[abc]%s", id);
。- 如果字符串 result 为 “apple123”,字符串 id 就会变成 “pple123”。
如果想控制读取字符的长度,可以在 % 和 [ 之间添加数字,e.g. %9[^abc]
。此外,字符集支持范围表示,e.g. %[a-c]
表示字符集为’a’
、’b’
或’c’
,%[a-c1-3]
表示字符集为'a'
、'b'
、'c'
和'1'
、'2'
、'3'
。
扫描集的缺点
实际上,扫描集的实际使用是及其复杂的,我们会用几个案例来说明。
案例一
#include <stdio.h>
int main()
{
char result[50] = "12345678, Mike Carter, 98, 77";
char id[9], name[20];
int grade_1, grade_2;
sscanf(result, "%9[^,], %20[^,], %d, %d", id, name, &grade_1, &grade_2);
printf("%s\n", id);
printf("%s\n", name);
printf("%d\n", grade_1);
printf("%d", grade_2);
}
上面代码中,字符串 result 代表的是一个学生的信息,内容为"12345678, Mike Carter, 98, 77"
,四个项代表学号、姓名、科目A成绩和科目B成绩。其中,明明每个项之间的分隔是一个逗号和一个空格,但是我们使用%[^, ]
格式说明符就会得到这样的输出:
12345678
Mike
32761
697031942
出现这种结果是因为,扫描集仅仅识别','
或' '
,而不是将", "
视为一个整体,这意味着读取进行到了 Mike 和 Carter 之间就会停止。因此,当 sscanf 尝试读取第一个%d 时,它实际上是在尝试从" Carter, 98, 77"
这个字符串开始读取整数,这自然会失败,因为" Carter"
并不是一个有效的整数表示。由于第一个 %d 转换失败,变量 grade_1 和 grade_2 将不会被赋予任何值(在这里它们是局部变量且未初始化,所以我们看到的 32761 和 697031942 可能是这块内存存放的垃圾值),或者函数 sscanf() 可能会返回少于 4 的值,表明转换过程中发生了错误。
扫描集中的字符从来不会以整体的形式存在,我们想实现整体的效果只能搭配一些复杂的扫描集组合,但这样做很麻烦。
案例二
假如字符串 result 现在是这样:"12345678#@Mike Carter#@98#@77"
。
如果我们调用语句sscanf(result, "%[^#@]#@%[^#@]#@%d, %d", id, name, &grade_1, &grade_2);
,得到的变量 grade_2 将不会存储正确的值,原因同上。只有调用语句sscanf(result, "%[^#@]#@%[^#@]#@%d#@%d", id, name, &grade_1, &grade_2);
才能获得理想的输出结果。不过这不是重点,重点在于,如果我们执行下面的代码:
#include <stdio.h>
int main()
{
char result[50] = "12345678#@Mike Carter#@98#@77";
char id[9], name[20];
int grade_1, grade_2;
sscanf(result, "%[^#@], %[^#@], %d, %d", id, name, &grade_1, &grade_2);
printf("%s\n", id);
printf("%s\n", name);
printf("%d\n", grade_1);
printf("%d", grade_2);
}
得到的输出结果为:
12345678
32761
697031942
这是因为,无论是 scanf() 函数还是 sscanf() 函数,每个格式符 %? 之间的内容代表的是分隔符。e.g. sscanf(str, "%d %d", a, b);
,两个 %d 之间是一个空格,所以字符串 str 中的数字应当用一个空格隔开,就像1 2
这样。但是在上述代码的第八行,每个格式符之间是用", "
而不是#@
隔开的,扫描集扫了一圈字符串 result 没找到", "
,因此字符串 name、变量 grade_1 和变量 grade_2 没有被赋任何值。
执行语句sscanf(result, "%[^#@]%[^#@]%d%d", id, name, &grade_1, &grade_2);
的结果是一样的,因为扫描集扫到 12345678 和 Mike Carter 之间的 #@、然后将 12345678 赋值给 id,这没有问题,但是我们并没有告诉 sscanf() 函数在 id 和 name 之间的分隔是 #@,于是扫描集又把 12345678 和 Mike Carter 之间的 #@ 扫了一遍,得到的字符串 name 自然什么都没有。然后变量 grade_1 和变量 grade_2 又触发了前一个案例的错误,得到两个垃圾值。
因此,想要获得理想的输出结果,我们只能执行语句sscanf(result, "%[^#@]#@%[^#@]#@%d#@%d", id, name, &grade_1, &grade_2);
。
案例三
使用赋值抑制字符同样会造成很多难以预料的结果。来看下列代码:
#include <stdio.h>
int main()
{
char result[50] = "flexible123";
char id[50];
if (sscanf(result, "%*[abc]%s", id) == 1)
printf("%s", id);
else
printf("sscanf()函数的执行根本就不成功!\n");
}
设计这段代码的目的在于,我们希望让字符串 result 一直被正常读取,读取到字符'a'
、'b'
或'c'
中的任意一个就跳过,然后继续读取。换句话说,我们以为这段代码能够输出 flexile123,但这段代码执行的结果为:
sscanf() 函数的执行根本就不成功!
没错,sscanf() 函数的执行失败了,我们甚至连 le123 都得不到。原因在于,%*[abc]
是跳过匹配的内容,但跳过的前提是先匹配,如果第一个字符就不匹配,扫描就不会继续了。这就好比我们用%[abc]
处理字符串 result,第一个字符就不匹配,sscanf() 函数也就不再处理这个字符串了。
有的同学可能有疑问:%*[abc]
没有匹配,但后面的%s
不是还在吗?为什么它不匹配呢?
这是因为,sscanf() 函数的工作原理就是:有几个字段被匹配,就返回对应的值;一旦无法匹配当前的格式说明符,它就停止处理,并且返回已经匹配和存储的字段数。在我们的示例代码中,虽然有%*[abc]
和%s
两个格式符,但这是一对组合,处理后只会给 id 这一个字符串赋值,sscanf() 函数返回的最多也只能是 1。所以,匹配失败意味着一损俱损,%*[abc]
和%s
都会“阵亡”。
另一方面,即便%*[abc]
和%s
是两个独立的格式符,二者只要有一个无法匹配,整个处理过程就会停止,并且不会继续处理后续的格式说明符 —— 这是 sscanf() 函数自己的特性,我们可以用下面的代码验证该特性:
#include <stdio.h>
int main()
{
char result[50] = "flexible123 apple123";
char id[10], str[10];
int a = sscanf(result, "%*[abc]%s %s", id, str);
printf("sscanf()返回值为:%d\n", a);
printf("id被赋值为:%s\n", id);
printf("str被赋值为:%s", str);
}
输出结果为:
sscanf()返回值为:0
id被赋值为:
str被赋值为:
总结
用扫描集搭配 sscanf() 函数处理字符串的方法的缺点在于不够灵活,它往往不是我们所想的那么好用。有时候我们利用 sscanf() 函数从文件中读取了一堆带有乱码的内容,想用 sscanf() 函数 + 扫描集可能不如调用 string 库中的字符串函数好使。