扫描集——%[]格式说明符

扫描集 & 字符集

基本概念

%[] 格式说明符是一种控制读取内容的工具,其专有名词是扫描集(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"
  • 假设我们执行这一语句: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"
  • 假设我们执行这一语句: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 库中的字符串函数好使。

  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值