1) 指定读取长度

还记得在 printf() 中可以指定最小输出宽度吗?就是在格式控制符的中间加上一个数字,例如,%10d表示输出的整数至少占用 10 个字符的位置:

  • 如果整数的宽度不足 10,那么在左边以空格补齐;
  • 如果整数的宽度超过了 10,那么以整数本身的宽度来输出,10 不再起作用。

其实,scanf() 也有类似的用法,也可以在格式控制符的中间加一个数字,用来表示读取数据的最大长度,例如:
%2d表示最多读取两位整数;
%10s表示读取的字符串的最大长度为 10,或者说,最多读取 10 个字符。

请看下面的例子:

  1. #include <stdio.h>
  2. int main(){
  3. int n;
  4. float f;
  5. char str[23];
  6. scanf("%2d", &n);
  7. scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区
  8. scanf("%5f", &f);
  9. scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区
  10. scanf("%22s", str);
  11. printf("n=%d, f=%g, str=%s\n", n, f, str);
  12. return 0;
  13. }

输入示例 ①:

20
100.5
http://c.biancheng.net
n=20, f=100.5, str=http://c.biancheng.net

输入示例 ②:

8920
10.2579
http://data.biancheng.net
n=89, f=10.25, str=http://data.biancheng.

这段代码使用了多个 scanf() 函数连续读取数据,为了避免受到缓冲区中遗留数据的影响,每次读取结束我们都使用scanf("%*[^\n]"); scanf("%*c");来清空缓冲区。

限制读取数据的长度在实际开发中非常有用,最典型的一个例子就是读取字符串:我们为字符串分配的内存是有限的,用户输入的字符串过长就存放不了了,就会冲刷掉其它的数据,从而导致程序出错甚至崩溃;如果被黑客发现了这个漏洞,就可以构造栈溢出攻击,改变程序的执行流程,甚至执行自己的恶意代码,这对服务器来说简直是灭顶之灾。

在用 gets() 函数读取字符串的时候,有一些编译器会提示不安全,建议替换为 gets_s() 函数,就是因为 gets() 不能控制读取到的字符串的长度,风险极高。

就目前学到的知识而言,虽然 scanf() 可以控制字符串的长度,但是字符串中却不能包含空白符,这是硬伤,所以 scanf() 暂时还无法替代 gets()。不过大家也不要着急,稍后我还会补充 scanf() 的高级用法,届时 scanf() 就可以完全替代 gets(),并且比 gets() 更加智能。

2) 匹配特定的字符

%s 控制符会匹配除空白符以外的所有字符,它有两个缺点:

  • %s 不能读取特定的字符,比如只想读取小写字母,或者十进制数字等,%s 就无能为力;
  • %s 读取到的字符串中不能包含空白符,有些情况会比较尴尬,例如,无法将多个单词存放到一个字符串中,因为单词之间就是以空格为分隔的,%s 遇到空格就读取结束了。

要想解决以上问题,可以使用 scanf() 的另外一种字符匹配方式,就是%[xxx][ ]包围起来的是需要读取的字符集合。例如,%[abcd]表示只读取字符abcd,遇到其它的字符就读取结束;注意,这里并不强调字符的顺序,只要字符在 abcd 范围内都可以匹配成功,所以你可以输入 abcd、dcba、ccdc、bdcca 等。

请看下面的代码:

  1. #include <stdio.h>
  2. int main(){
  3. char str[30];
  4. scanf("%[abcd]", str);
  5. printf("%s\n", str);
  6. return 0;
  7. }

输入示例 ①:

abcdefgh
abcd

输入示例 ②:

baccbaxyz
baccba

使用连接符

为了简化字符集合的写法,scanf() 支持使用连字符-来表示一个范围内的字符,例如 %[a-z]、%[0-9] 等。

连字符左边的字符对应一个 ASCII 码,连字符右边的字符也对应一个 ASCII 码,位于这两个 ASCII 码范围以内的字符就是要读取的字符。注意,连字符左边的 ASCII 码要小于右边的,如果反过来,那么它的行为是未定义的。

常用的连字符举例:

  • %[a-z]表示读取 abc...xyz 范围内的字符,也即小写字母;
  • %[A-Z]表示读取 ABC...XYZ 范围内的字符,也即大写字母;
  • %[0-9]表示读取 012...789 范围内的字符,也即十进制数字。

你也可以将它们合并起来,例如:

  • %[a-zA-Z]表示读取大写字母和小写字母,也即所有英文字母;
  • %[a-z-A-Z0-9]表示读取所有的英文字母和十进制数字;
  • %[0-9a-f]表示读取十六进制数字。

请看下面的演示:

  1. #include <stdio.h>
  2. int main(){
  3. char str[30];
  4. scanf("%[a-zA-Z]", str); //只读取字母
  5. printf("%s\n", str);
  6. return 0;
  7. }

输入示例:

abcXYZ123
abcXYZ

不匹配某些字符

假如现在有一种需求,就是读取换行符以外的所有字符,或者读取 0~9 以外的所有字符,该怎么实现呢?总不能把剩下的字符都罗列出来吧,一是麻烦,二是不现实。

C语言的开发者们早就考虑到这个问题了,scanf() 允许我们在%[ ]中直接指定某些不能匹配的字符,具体方法就是在不匹配的字符前面加上^,例如:

  • %[^\n]表示匹配除换行符以外的所有字符,遇到换行符就停止读取;
  • %[^0-9]表示匹配除十进制数字以外的所有字符,遇到十进制数字就停止读取。

请看下面的例子:

  1. #include <stdio.h>
  2. int main(){
  3. char str1[30], str2[30];
  4. scanf("%[^0-9]", str1);
  5. scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区
  6. scanf("%[^\n]", str2);
  7. printf("str1=%s \nstr2=%s\n", str1, str2);
  8. return 0;
  9. }

输入示例:

abcXYZ@#87edf
c c++ java python go javascript
str1=abcXYZ@#
str2=c c++ java python go javascript

请注意第 6 行代码,它的作用是读取一行字符串,和 gets() 的功能一模一样。你看,scanf() 也能读取带空格的字符串呀,谁说 scanf() 不能完全取代 gets(),这明显是错误的说法。

另外,scanf() 还可以指定字符串的最大长度,指定字符串中不能包含哪些字符,这是 gets() 不具备的功能。

例如,读取一行不能包含十进制数字的字符串,并且长度不能超过 30:

  1. #include <stdio.h>
  2. int main(){
  3. char str[31];
  4. scanf("%30[^0-9\n]", str);
  5. printf("str=%s\n", str);
  6. return 0;
  7. }

输入示例 ①:

http://c.biancheng.net http://biancheng.net
str=http://c.biancheng.net http://

输入示例 ②:

I have been programming for 8 years.
str=I have been programming for

总之,scanf() 不仅可以完全替代 gets(),并且比 gets() 的功能更加强大。

3) 丢弃读取到的字符

在前面的代码中,每个格式控制符都要对应一个变量,把读取到的数据放入对应的变量中。其实你也可以不这样做,scanf() 允许把读取到的数据直接丢弃,不往变量中存放,具体方法就是在 % 后面加一个*,例如:

  • %*d表示读取一个整数并丢弃;
  • %*[a-z]表示读取小写字母并丢弃;
  • %*[^\n]表示将换行符以外的字符全部丢弃。

请看下面的代码演示:

  1. #include <stdio.h>
  2. int main(){
  3. int n;
  4. char str[30];
  5. scanf("%*d %d", &n);
  6. scanf("%*[a-z]");
  7. scanf("%[^\n]", str);
  8. printf("n=%d, str=%s\n", n, str);
  9. return 0;
  10. }

输入示例:

100 999abcxyzABCXYZ
n=999, str=ABCXYZ

对结果的分析:整数 100 被第一个 scanf() 中的%*d读取后丢弃了,整数 999 被第%d读取到,并赋值给 n。此时缓冲区中剩下 abcxyzABCXYZ,第二个 scanf() 将 abcxyz 读取并丢弃,剩下的 ABCXYZ 被最后一个 scanf() 读取到并赋值给 str。

大家有没有意识到,将读取到的字符直接丢弃,这就是在清空输入缓冲区呀,虽然有点蹩脚,但是行之有效。在《清空(刷新)缓冲区,从根本上消除那些奇怪的行为》一节中,我们已经给出了使用 scanf() 清空缓冲区的方案,就是:

scanf("%*[^\n]"); scanf("%*c");

下面我们就来解释一下。

首先需要明白的是,等到需要清空缓冲区的时候,缓冲区中的最后一个字符一定是换行符\n,因为输入缓冲区是行缓冲模式,用户按下回车键会产生换行符,结束本次输入,然后输入函数开始读取。

scanf("%*[^\n]");将换行符前面的所有字符清空,scanf("%*c");将最后剩下的换行符清空。

有些网友将这两条语句合并起来,写作:

scanf("%*[^\n]%*c");

这是错误的。合并以后的语句不能清空单个换行符,因为该语句要求换行符前边至少要有一个其它的字符,单个换行符会导致匹配失败。

总结

scanf() 控制字符串的完整写法为:

%{*} {width} type

其中,{ } 表示可有可无。各个部分的具体含义是:

    • type表示读取什么类型的数据,例如 %d、%s、%[a-z]、%[^\n] 等;type 必须有。
    • width表示最大读取宽度,可有可无。
    • *表示丢弃读取到的数据,可有可无。

c语言:scanf()高级应用的更多相关文章

  1. C语言 scanf()和gets()函数的区别

    C语言 scanf()和gets()函数的区别 1.相同点:scanf( )函数和gets( )函数都可用于输入字符串 2.不同点:两者在功能上有所区别,具体区别如下: 要实现如下需求“从控制台输入字 ...

  2. C语言scanf与get char,gets的区别

    C语言scanf与get char,gets的区别 1.scanf() scanf是C语言的格式输入函数是通用终端格式化输入函数,它从标准输入设备(键盘) 读取输入的信息.可以读入任何固有类型的数据并 ...

  3. 大数据笔记(二十六)——Scala语言的高级特性

    ===================== Scala语言的高级特性 ========================一.Scala的集合 1.可变集合mutable 不可变集合immutable / ...

  4. C语言scanf函数详细解释

    原文链接 函数名: scanf 功 能: 执行格式化输入 用 法: int scanf(char *format[,argument,...]); scanf()函数是通用终端格式化输入函数,它从标准 ...

  5. C语言Scanf函数

    C语言的scanf函数 一.变量的内存分析 (一)字节与地址 ①. 内存以字节为单位 每个字节都有自己的内存地址,根据地址就可以找到该字节.整个内存相当于一整个酒店,而酒店以房间为单位,在这里每个房间 ...

  6. C语言--scanf

    关于C语言的scanf,首先看个例子 int get_int(void){ int input; char ch; ){ printf("is not an integer,please e ...

  7. [转载]VS2012编译C语言scanf函数error的解决方法

    在VS 2012 中编译 C 语言项目,如果使用了 scanf 函数,编译时便会提示如下错误: error C4996: 'scanf': This function or variable may ...

  8. C语言scanf函数详解

    函数名: scanf  功 能: 运行格式化输入  用 法: int scanf(char *format[,argument,...]); scanf()函数是通用终端格式化输入函数,它从标准输入设 ...

  9. c语言scanf详解

    函数名: scanf 功 能: 执行格式化输入 用 法: int scanf(char *format[,argument,...]);scanf()函数是通用终端格式化输入函数,它从标准输入设备(键 ...

随机推荐

  1. Docker学习(10) Docker的远程访问

    Docker的远程访问 只需配置

  2. CVPR2018论文看点:基于度量学习分类与少镜头目标检测

    CVPR2018论文看点:基于度量学习分类与少镜头目标检测 简介 本文链接地址:https://arxiv.org/pdf/1806.04728.pdf 距离度量学习(DML)已成功地应用于目标分类, ...

  3. L3级自动驾驶

    L3级自动驾驶 2020年开年 3月9日,工信部在其官网公示了<汽车驾驶自动化分级>推荐性国家标准报批稿,并拟于2021年1月1日开始实施. 按照中国自身标准制定的自动驾驶分级标准,在千呼 ...

  4. Wide & Deep的OneFlow网络训练

    Wide & Deep的OneFlow网络训练 HugeCTR是英伟达提供的一种高效的GPU框架,专为点击率(CTR)估计训练而设计. OneFlow对标HugeCTR搭建了Wide & ...

  5. CodeGen字段循环Field Loop

    CodeGen字段循环Field Loop 字段循环是一个模板文件构造,它允许迭代CodeGen拥有的有关字段的集合.这些字段定义可以来自以下两个位置之一: •如果基于从存储库结构中获取的信息生成代码 ...

  6. 深入 Go 中各个高性能 JSON 解析库

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/535 其实本来我是没打算去看 JSON 库的性能问题的,但是最近我对 ...

  7. Python集合:set

    集合 集合的描述 set是一个无序不重复的序列,可以用{}或者 set() 函数创建集合,它存放不可变类型(如字符串.数字.元组)数据. 注意:创建一个空集合必须使用set()方法,因为{}是用来生成 ...

  8. mybatis学习——类型别名(typeAliases)

    为什么要用类型别名? 答:类型别名可为 Java 类型设置一个缩写名字. 它仅用于 XML 配置,意在降低冗余的全限定类名书写. 举个例子说明: 在我们编写映射文件的时候: <?xml vers ...

  9. Go语言实现的23种设计模式之结构型模式

    摘要:本文主要聚焦在结构型模式(Structural Pattern)上,其主要思想是将多个对象组装成较大的结构,并同时保持结构的灵活和高效,从程序的结构上解决模块之间的耦合问题. 本文分享自华为云社 ...

  10. 办公利器!用Python快速将任意文件转为PDF

    痛点: 相信大家都会遇到一种场景.老师/上司要求你把某个文件转为pdf,并且是一批(不止一个,一个的话手动就可以搞定),并且这种是枯燥无聊的工作,既没有什么技术含量又累. 试想一下,如果我把这些文件放 ...