简记清空C语言输入残留内容
为了在命令行程序中实现和用户的交互,我们编写的程序的运行过程中往往涉及到对标准输入/输出流的多次读写。
在C语言中接受用户输入这一块,有着一个老生常谈的问题:“怎么样及时清空输入流中的数据?”
这也是这篇小笔记的主题内容。
先从缓冲区说起。
缓冲区是内存中划分出来的一部分。通常来说,缓冲区类型有三种:
- 全缓冲
- 行缓冲
- 无缓冲
行缓冲
在C语言中缓冲区这个概念的存在感还是挺强的,比较常用到的缓冲区类型则是行缓冲了,如标准输入流 stdin
和标准输出流 stdout
一般(终端环境下)就是在行缓冲模式下的。
行缓冲,顾名思义,就是针对该缓冲区的I/O操作是基于行的。
在遇到换行符前,程序的输入和输出都会先被暂存到流对应的缓冲区中
而在遇到换行符后(或者缓冲区满了),程序才会进行真正的I/O操作,将该缓冲区中的数据写到对应的流 (stream) 中以供后续读取。
就标准输入stdin
而言,用户的输入首先会被存到相应的输入缓冲区中,每当用户按下回车键输入一个换行符,程序才会进行I/O操作,将缓冲区暂存的数据写入到stdin
中,以供输入函数使用。
而对标准输出stdout
来说,输出内容也首先会被暂存到相应的输出缓冲区中,每当输出数据遇到换行符时,程序才会将缓冲区中的数据写入stdout
,继而打印到屏幕上。
这也是为什么在缓冲模式下,输出的内容不会立即打印到屏幕上:
#include <stdio.h>
int main()
{
// 设置缓冲模式为行缓冲,缓冲区大小为10字节
setvbuf(stdout, NULL, _IOLBF, 10);
fprintf(stdout, "1234567"); // 这里先向stdout对应的缓冲区中写入了7字节
getchar(); // 这里等待用户输入
printf("89"); // 再向stdout对应的缓冲区中写入了2字节
getchar(); // 接着等待用户输入
printf("Print!"); // 再向stdout对应的缓冲区中写入了6字节
getchar(); // 最后再等待一次用户输入
return 0;
}
运行效果:
可以看到,直到执行到第二个getchar()
时,屏幕上没有新的输出。
而在执行了printf("Print!")
之后,输出缓冲区被填满了,输出缓冲区中现有的10
字节的数据被写入到stdout
中,继而才在屏幕上打印出123456789P
。
缓冲区内容被读走后,剩余的字符串rint!
接着被写入输出缓冲区。程序运行结束后,输出缓冲区中的内容会被全部打印到屏幕上,所以会在最后看到rint!
。
C语言中常用的输入函数
输入函数做的工作主要是从文件流中读取数据,亦可将读取到的数据储存到内存中以供后续程序使用。
基于字符
// 从给定的文件流中读一个字符 (fgetc中的 f 的意思即"function")
int fgetc( FILE *stream );
// 同fgetc,但是getc的实现*可能*是基于宏的
int getc( FILE *stream );
// 相当于是getc(stdin),从标准输入流读取一个字符
int getchar(void);
// 返回获取的字符的ASCII码值,如果到达文件末尾就返回EOF(即返回-1)
基于行
// 从给定的文件流中读取(count-1)个字符或者读取直到遇到换行符或者EOF
// fgets中的f代表“file”,而s代表“string”
char *fgets( char *restrict str, int count, FILE *restrict stream );
// 返回指向字符串的指针或者空指针NULL
格式化输入
// 按照format的格式从标准输入流stdin中读取所需的数据并储存在相应的变量中
// scanf中的f代表“format”
int scanf( const char *restrict format, ... );
// 按照format的格式从文件流stream中读取所需的数据并储存在相应的变量中
// fscanf中前一个f代表“file(stream)”,后一个f代表“format”
int fscanf( FILE *restrict stream, const char *restrict format, ... );
// 按照format的格式从字符串buffer中截取所需的数据并储存在相应的变量中
// sscanf中的第一个s代表“string”,字符串
int sscanf( const char *restrict buffer, const char *restrict format, ... );
// 返回一个整型数值,代表成功根据格式赋值的变量数(arguments)
最常到的输入流问题
先来个不会出问题的示例:
#include <stdio.h>
int main()
{
char test1[200];
char test2[200];
char testChar;
printf("Input a Character: \n");
testChar = getchar();
fprintf(stdout, "Input String1: \n");
scanf("%s", test1);
fprintf(stdout, "Input String2: \n");
scanf("%s", test2);
printf("Got String1: [ %s ]\n", test1);
printf("Got String2: [ %s ]\n", test2);
printf("Got Char: [ %c ]\n", testChar);
return 0;
}
运行效果:
出问题的示例:
#include <stdio.h>
int main()
{
char test[200];
char testChar1, testChar2, testChar3;
fprintf(stdout, "Input String: \n");
scanf("%3s", test);
printf("[1]Input a Character: \n");
testChar1 = getchar();
printf("[2]Input a Character: \n");
testChar2 = fgetc(stdin);
printf("[3]Input a Character: \n");
testChar3 = getchar();
printf("Got String: [ %s ]\n", test);
printf("Got Char1: [ %c ]\n", testChar1);
printf("Got Char2: [ %c ]\n", testChar2);
printf("Got Char3: [ %c ]\n", testChar3);
return 0;
}
运行效果:
因为我将格式设置为了%3s
,所以scanf
最多接收包含三个字符的字符串。
在这个示例中,我按要求输入了一条字符串Hello
,并按下回车输入一个换行符,缓冲区数据Hello\n
被写入到了stdin
中。而scanf
只从标准流stdin
中读走了Hel
这一部分字符串。
此时,标准流stdin
中实际上还剩3个字符:
l
o
\n
(回车输入的换行符)
于是接下来三次针对字符的输入函数只会分别从stdin
中取走这三个字符,而不会等待用户输入,这就没有达到我想要的效果。
在基本的命令行程序中很容易遇到这类问题,这也是为什么需要及时清空输入流stdin
中的数据。
如何处理残余内容
以下内容假设stdout
和stdin
两个标准流都是在行缓冲模式下的。
标准输出流stdout
虽然本文主要是写输入流,但这里我还是掠过一下标准输出流stdout
。C语言标准库中提供了一个用于刷新输出流缓冲区的函数:
int fflush( FILE *stream );
// 如果成功了,返回0,否则返回EOF(-1)
要清空标准输出流对应的缓冲区,只需要使用fflush(stdout)
即可。上面的这个例子可以修改成这样:
#include <stdio.h>
int main()
{
// 设置缓冲模式为行缓冲,缓冲区大小为10字节
setvbuf(stdout, NULL, _IOLBF, 10);
fprintf(stdout, "1234567"); // 这里先向stdout对应的缓冲区中写入了7字节
fflush(stdout); // 刷新缓冲区,将缓冲区中的数据写入到标准输出流中
getchar(); // 这里等待用户输入
printf("89"); // 再向stdout对应的缓冲区中写入了2字节
fflush(stdout);
getchar(); // 接着等待用户输入
printf("Print!"); // 再向stdout对应的缓冲区中写入了6字节
getchar(); // 最后再等待一次用户输入
return 0;
}
运行效果:
可以看到,加入fflush(stdout)
后,输出缓冲区的内容会被及时写入stdout
中,继而打印到屏幕上。
值得注意的是,fflush(stdin)
的行为是未定义(不确定)的:
For input streams (and for update streams on which the last operation was input), the behavior is undefined.
不同平台的编译器对此有不同的解释。
比如在Windows平台上,无论是
VC6.0
这种目前一些学校教学还在使用的古董编译器,还是gcc 8.x.x
,大体还是支持通过这种操作清空输入流的。但是在Linux平台上的
gcc
编译器就不买账了,是不支持fflush(stdin)
这种操作的。
因此,尽量避免fflush(stdin)
这种写法,这十分不利于代码的可移植性。
标准输入流stdin
上面提到因为可移植性要避免fflush(stdin)
这种写法,接下来记录一下可移植性高的写法。
接受格式化输入时去除多余空白符
这一种其实用的比较少,但我觉得还是得记一下。
whitespace characters: any single whitespace character in the format string consumes all available consecutive whitespace characters from the input. Note that there is no difference between "\n", " ", "\t\t", or other whitespace in the format string.
上面这段解释来自于cppreference,也就是说,格式化字符串中的空白符(如"\n"
, " "
, "\t\t"
)会吸收输入字符串中的一段连续的空白符。
也就是说,下面这句格式化输入函数:
scanf(" %c %c",&recvChar1,&recvChar2);
可以从stdin
中读取形如 \n a b
, \t a b
这样的数据。其中a
之前的空白符和a
与b
之间的空白符都会被吸收,scanf
得以能准确获取字符a
和b
。
依靠这个特性,我们可以在接收输入时自动剔除stdin
中残留的空白符:
// 因为格式%s不会匹配多余的空白符,这里按回车后,stdin中会残留一个换行符\n
scanf("%s",recvStr);
// 在格式%c前加一个空格,可以吸收掉上面残留的换行符\n,程序便能如预期接受用户输入
scanf(" %c",&recvChar);
然而,这一种方法仅只能剔除多余的空白符。
使用中括号字符集
这个解决方法可以和上面剔除空白符的方法进行结合。
格式化输入有一个说明符 %[set]
,它的功能和正则表达式中的中括号[ ]
十分类似:
其中
set
代表一个用于匹配的字符集,一般情况下匹配的是存在字符集中的字符字符集的第一个字符如果是
^
,则表示取反,匹配的是不存在于该字符集中的字符可以在中括号中使用短横线
-
来表达一个范围,比如%[0-9]
代表匹配0-9之间的字符。值得注意的是,对于短横线-
,可能在不同编译器之间有不同实现,它是implementation-defined的。
另还有一个说明符 *
,它被称为赋值抑制或赋值屏蔽符。如字面意思,在%
引导的格式转换字串中如果包含*
,这个格式匹配的内容不会被赋给任何变量。
于是,可以给出如下的语句:
// 星号 * 代表不会把匹配到的内容赋给变量,相当于“吸收”掉了
// [^\n] 代表除了换行符外一律匹配
scanf("%*[^\n]");
因为用户结束一次输入的标志通常是按回车输入一个换行符,残留的内容往往末尾是一个换行符。上面这句的原理就是吸收掉stdin
中所有的残余字符,直至达到最后一个字符,也就是换行符。
然而,换行符不会被上面这句所吸收,所以在接下来的输入中只需要忽略stdin
中的残余空白符即可(换行符就是空白符之一):
scanf("%*[^\n]");
scanf(" %c",&recvChar);
这种方法已经可以解决一般情况下的输入残余问题,不过在后续接受格式化输入时还得忽略换行符\n
,还是有点麻烦。
循环取走残余字符
这一种方法能在清除残余时顺便吸收掉末尾的换行符\n
。
取字符需要用到取单个字符的输入函数,这里为了方便,选用的是getchar()
。
一般情况下可以这样写:
// getchar() 会从 stdin 中取走一个字符
while(getchar() != '\n')
;
(使用前提:stdin
中有残余)
while
循环会一直进行,直至getchar()
取到的字符为换行符\n
为止,这样就可以顺带吸收掉末尾的换行符了,能相对完美地清除掉stdin
中的残余内容。
(在行缓冲模式下,用户的一次输入通常以一个换行符结束)
不过咧,还可以考虑更周全点。在getchar()
获取字符失败的时候会返回EOF
,但此时并不满足while
循环的退出条件,对此可以再完善一下:
// 临时储存字符
// 之所以是整型(int),是因为EOF是一个代表 负值整型(通常为-1) 的宏
int tempChar;
// tempChar=getchar()这种赋值语句本身的返回值就是所赋的值
while ((tempChar = getchar()) != '\n' && tempChar != EOF)
;
这样一来,当getchar()
失败时,程序执行就会跳出循环。
综上,针对stdin
中的残余内容的清除,最建议采用的便是最后这种处理方法。
不过其他的方法也是可以在一些场景中使用的,这就见仁见智了...
什么时候会返回EOF
这里提一个题外的点:什么时候getchar()
会返回EOF
?再进一步想,什么时候程序会认为标准流stdin
达到了文件流末尾?
实际上,这里的EOF
往往是用户输入的一个特殊二进制值[3],输入方式:
在Windows系统下是 Ctrl + Z(F6应该也行)
在Linux下是 Ctrl + D
当用户在输入中发送EOF
时,标准流stdin
就会被标记为EOF
,因此getchar()
就会获取字符失败而返回EOF
。
// 测试用代码
#include <stdio.h>
int main()
{
char testChar;
fprintf(stdout, "Input Char: \n");
testChar = getchar();
if (testChar == EOF)
{
printf("Received EOF\n");
}
else
{
printf("Received a char\n");
}
return 0;
}
EOF
在C语言中是一个宏,定义在头文件stdio.h
中,其值为一个负值的整型(并不一定是 -1
),因此上面用tempChar != EOF
来判断getchar()
失败。
处理残余的语句放在哪里
现在咱已经搞清楚了清除残余的代码,那么这些代码该放在哪呢?
对于标准输出流stdout
来说,fflush
语句往往放在输出函数执行完成之后,以立刻将输出内容打印到屏幕上:
printf("Hello ");
printf("World!\n");
fflush(stdout);
当然,如果嫌麻烦可以在输出前直接通过setbuf
关闭stdout
的缓冲:
setbuf(stdout, NULL);
对于标准输入流stdin
来说,处理残余的语句往往放在每次输入函数执行之后,以及时清理流中残余内容:
int c;
char testChar1, testChar2;
scanf("%*s"); // * 用于屏蔽赋值
while ((c = getchar()) != '\n' && c != EOF)
;
testChar1 = getchar();
while ((c = getchar()) != '\n' && c != EOF)
;
scanf("%c", &testChar2);
当然,这样就显得有点冗余了。
实际上可以将清除的语句封装进函数或者定义为宏(不过确实不太建议定义为宏),这样也更便于维护。
总结
之前浏览了很多相关文章,标题和内容大多都写着“清空输入缓冲区”。现在想一下,这样写可能是不对的,因为实际我清空的是标准输入流stdin
中的残留内容。在用户输入完成(输入换行符)的那一刻,输入缓冲区实际上就已经被清空了。
也就是说,标准流和对应的缓冲区要辨别清楚,二者不是同一个概念(一个stream
一个buffer
),千万不能混淆了。
最后,感谢你看到这里~
本笔记可能还是有错误出现,也请各位多指教!
参考文献
本笔记相关:
关于"implementation-defined"
简记清空C语言输入残留内容的更多相关文章
- Python+Selenium自动化-清空输入框、输入内容、点击按钮
Python+Selenium自动化-清空输入框.输入内容.点击按钮 1.输入内容 send_keys('valve'):输入内容valve #定位输入框 input_box = browser. ...
- 【转载】C#检测客户端输入的内容是否含有危险字符串
用户在客户端提交的内容有时候并不可信,如果客户端提交的内容中含有危险字符串信息,则很有可能造成应用程序安全性问题,如SQL注入风险等.因此在接收客户端提交过来的数据后,我们首先需要判断数据中是否含有危 ...
- 【转】Linux 中清空或删除大文件内容的五种方法(truncate 命令清空文件)
原文: http://www.jb51.net/article/100462.htm truncate -s 0 access.log -------------------------------- ...
- [JS] 文本框判断输入的内容是否为数字
可以通过触发文本框的onchange事件来对输入的内容进行判断是否为数字 文本框的属性设置: 把onchange的属性对应的js函数写好即可 参数传输的是当前控件的value值,即text值 < ...
- mailto实现将用户在网页中输入的内容传递到本地邮件客户端
背景: 想在自己的网站中有这样一个设计: 用户点击提交按钮之后,就会打开本地邮件客户端,并自动将他在输入框中输入的内容作为邮件的内容,像下面这样: mailto可以帮助实现这个功能. 简介: mail ...
- PHP批量清空删除指定文件夹内容
PHP批量清空删除指定文件夹内容: cleancache.php <?php // 清文件缓存 $dirs = array( realpath(dirname(__FILE__) . '/../ ...
- jquery+php实现用户输入搜索内容时自动提示
index.html <html> <head> <meta charset=;} #search_auto li a:hover{background:#D8D ...
- Android控件之MultiAutoCompleteTextView(自动匹配输入的内容)
一.功能 可支持选择多个值(在多次输入的情况下),分别用分隔符分开,并且在每个值选中的时候再次输入值时会自动去匹配,可用在发送短信,发邮件时选择联系人这种类型中 二.独特属性 android:comp ...
- Android控件之AutoCompleteTextView(自动匹配输入的内容)
一.功能 动态匹配输入的内容,如百度搜索引擎当输入文本时,可以根据内容显示匹配的热门信息 二.独特属性 android:completionThreshold = "2" — ...
随机推荐
- 攻防世界-MISC:坚持60s
这是攻防世界新手练习区的第六题,题目如下: 点击附件1下载,是一个java文件,点击运行一下: 绿帽子满天飞不知道是怎么回事(还是老老实实去看WP吧),WP说这是编译过的Java代码,但我手里没有反编 ...
- XCTF练习题---MISC---a_good_idea
XCTF练习题---MISC---a_good_idea flag:NCTF{m1sc_1s_very_funny!!!} 解题步骤: 1.观察题目,下载附件 2.到手以后发现是一张图片,尝试修改文件 ...
- 如何对用户的绑定的身份证真实性进行实名认证(java)
现在随着对用户实名制的要求,因此用户提交的身份证信息经查需要检查是否为真实信息,我们需要对用户提交的身份证信息进行核验,具体操作步骤如下: 第一步 到认证平台注册账号:云亿互通--实名认证服务 (yu ...
- vmware ubuntu 忘记密码
1.进入recovery模式 修改root密码 1.启动ubuntu系统,一开始在有进度条的时候按下shift键,出现GRUB选择菜单,选择Advanced options for Ubuntu 按回 ...
- systemd进程管理工具实战教程
关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 1. systemd介绍 systemd是目前Linux系统上主要的系统守护进程管理工具,由于 ...
- Java 效率工具, 大幅度提高开发效率
你是否有遇到过这样的情况,在开发过程中需要比较两列数据,但使用文本比对工具的话他是按行基准比对的,我还得对每列数据先进行排序,但排序又去哪里排, 想到 excel 可以排序 , 折腾下来,特别麻烦, ...
- Nginx的mirror指令能干啥?
mirror 流量复制 Nginx的 mirror 指令来自于 ngx_http_mirror_module 模块 Nginx Version > 1.13.4 mirror 指令提供的核心功能 ...
- 我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!
你好呀,我是歪歪. 前几天有朋友给我发来这样的一个截图: 他说他不理解,为什么这样不报错. 我说我也不理解,把一个 boolean 类型赋值给 int 类型,怎么会不报错呢,并接着追问他:这个代码截图 ...
- MySQL 的 EXPLAIN 语句及用法
在MySQL中 DESCRIBE 和 EXPLAIN 语句是相同的意思.DESCRIBE 语句多用于获取表结构,而 EXPLAIN 语句用于获取查询执行计划(用于解释MySQL如何执行查询语句). 通 ...
- 【freertos】009-任务控制
目录 前言 9.1 相对延时 9.1.1 函数原型 9.1.2 函数说明 9.1.3 参考例子 9.2 绝对延时 9.2.1 函数原型 9.2.2 函数说明 9.2.3 参考例子 9.3 获取任务优先 ...