为了在命令行程序中实现和用户的交互,我们编写的程序的运行过程中往往涉及到对标准输入/输出流的多次读写。

在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个字符:

  1. l
  2. o
  3. \n (回车输入的换行符)

于是接下来三次针对字符的输入函数只会分别stdin取走这三个字符,而不会等待用户输入,这就没有达到我想要的效果。

在基本的命令行程序中很容易遇到这类问题,这也是为什么需要及时清空输入流stdin中的数据

如何处理残余内容

以下内容假设stdoutstdin两个标准流都是在行缓冲模式下的。

标准输出流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之前的空白符和ab之间的空白符都会被吸收,scanf得以能准确获取字符ab

依靠这个特性,我们可以在接收输入时自动剔除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),千万不能混淆了。

最后,感谢你看到这里~

本笔记可能还是有错误出现,也请各位多指教!

参考文献

本笔记相关:

  1. File input/output - cppreference.com

  2. Clarify the difference between input/output stream and input/output buffer - StackOverflow

  3. End of File in stdin - StackOverflow

关于"implementation-defined"

简记清空C语言输入残留内容的更多相关文章

  1. Python+Selenium自动化-清空输入框、输入内容、点击按钮

    Python+Selenium自动化-清空输入框.输入内容.点击按钮   1.输入内容 send_keys('valve'):输入内容valve #定位输入框 input_box = browser. ...

  2. 【转载】C#检测客户端输入的内容是否含有危险字符串

    用户在客户端提交的内容有时候并不可信,如果客户端提交的内容中含有危险字符串信息,则很有可能造成应用程序安全性问题,如SQL注入风险等.因此在接收客户端提交过来的数据后,我们首先需要判断数据中是否含有危 ...

  3. 【转】Linux 中清空或删除大文件内容的五种方法(truncate 命令清空文件)

    原文: http://www.jb51.net/article/100462.htm truncate -s 0 access.log -------------------------------- ...

  4. [JS] 文本框判断输入的内容是否为数字

    可以通过触发文本框的onchange事件来对输入的内容进行判断是否为数字 文本框的属性设置: 把onchange的属性对应的js函数写好即可 参数传输的是当前控件的value值,即text值 < ...

  5. mailto实现将用户在网页中输入的内容传递到本地邮件客户端

    背景: 想在自己的网站中有这样一个设计: 用户点击提交按钮之后,就会打开本地邮件客户端,并自动将他在输入框中输入的内容作为邮件的内容,像下面这样: mailto可以帮助实现这个功能. 简介: mail ...

  6. PHP批量清空删除指定文件夹内容

    PHP批量清空删除指定文件夹内容: cleancache.php <?php // 清文件缓存 $dirs = array( realpath(dirname(__FILE__) . '/../ ...

  7. jquery+php实现用户输入搜索内容时自动提示

    index.html <html> <head>     <meta charset=;} #search_auto li a:hover{background:#D8D ...

  8. Android控件之MultiAutoCompleteTextView(自动匹配输入的内容)

    一.功能 可支持选择多个值(在多次输入的情况下),分别用分隔符分开,并且在每个值选中的时候再次输入值时会自动去匹配,可用在发送短信,发邮件时选择联系人这种类型中 二.独特属性 android:comp ...

  9. Android控件之AutoCompleteTextView(自动匹配输入的内容)

    一.功能 动态匹配输入的内容,如百度搜索引擎当输入文本时,可以根据内容显示匹配的热门信息 二.独特属性 android:completionThreshold = "2"    — ...

随机推荐

  1. 攻防世界-MISC:坚持60s

    这是攻防世界新手练习区的第六题,题目如下: 点击附件1下载,是一个java文件,点击运行一下: 绿帽子满天飞不知道是怎么回事(还是老老实实去看WP吧),WP说这是编译过的Java代码,但我手里没有反编 ...

  2. XCTF练习题---MISC---a_good_idea

    XCTF练习题---MISC---a_good_idea flag:NCTF{m1sc_1s_very_funny!!!} 解题步骤: 1.观察题目,下载附件 2.到手以后发现是一张图片,尝试修改文件 ...

  3. 如何对用户的绑定的身份证真实性进行实名认证(java)

    现在随着对用户实名制的要求,因此用户提交的身份证信息经查需要检查是否为真实信息,我们需要对用户提交的身份证信息进行核验,具体操作步骤如下: 第一步 到认证平台注册账号:云亿互通--实名认证服务 (yu ...

  4. vmware ubuntu 忘记密码

    1.进入recovery模式 修改root密码 1.启动ubuntu系统,一开始在有进度条的时候按下shift键,出现GRUB选择菜单,选择Advanced options for Ubuntu 按回 ...

  5. systemd进程管理工具实战教程

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 1. systemd介绍 systemd是目前Linux系统上主要的系统守护进程管理工具,由于 ...

  6. Java 效率工具, 大幅度提高开发效率

    你是否有遇到过这样的情况,在开发过程中需要比较两列数据,但使用文本比对工具的话他是按行基准比对的,我还得对每列数据先进行排序,但排序又去哪里排, 想到 excel 可以排序 , 折腾下来,特别麻烦, ...

  7. Nginx的mirror指令能干啥?

    mirror 流量复制 Nginx的 mirror 指令来自于 ngx_http_mirror_module 模块 Nginx Version > 1.13.4 mirror 指令提供的核心功能 ...

  8. 我怀疑这是IDEA的BUG,但是我翻遍全网没找到证据!

    你好呀,我是歪歪. 前几天有朋友给我发来这样的一个截图: 他说他不理解,为什么这样不报错. 我说我也不理解,把一个 boolean 类型赋值给 int 类型,怎么会不报错呢,并接着追问他:这个代码截图 ...

  9. MySQL 的 EXPLAIN 语句及用法

    在MySQL中 DESCRIBE 和 EXPLAIN 语句是相同的意思.DESCRIBE 语句多用于获取表结构,而 EXPLAIN 语句用于获取查询执行计划(用于解释MySQL如何执行查询语句). 通 ...

  10. 【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 获取任务优先 ...