{(MainTitle)} 简记C语言清空输入残留内容-Bottle小站 {(MainTitleEnd)} {(PostTitle)}简记C语言清空输入残留内容{(PostTitleEnd)} {(PostDate)}20220714{(PostDateEnd)} {(PostContent)} 为了在命令行程序中实现和用户的交互,我们编写的程序的运行过程中往往涉及到对标准输入/输出流的多次读写。 在C语言中接受用户输入这一块,有着一个老生常谈的问题:“怎么样及时清空输入流中的数据?” 这也是这篇小笔记的主题内容。 ![idling-2022-06-28](https://assets.xbottle.top/img/idling-2022-06-28.png) <bblock style='display:none'> { "src":"https://api.xbottle.top/ne/url/1939401645", "cover":"https://api.xbottle.top/ne/cover/1939401645", "title":"回夏", "artist":"cadode", "loop":"false", "float":"right" } </bblock> 先从缓冲区说起。 缓冲区是**内存中**划分出来的一部分。通常来说,缓冲区类型有三种: * 全缓冲 * 行缓冲 * 无缓冲 ## 行缓冲 在C语言中缓冲区这个概念的存在感还是挺强的,比较常用到的缓冲区类型则是**行缓冲**了,如标准输入流 `stdin` 和标准输出流 `stdout`一般(终端环境下)就是在行缓冲模式下的。 行缓冲,顾名思义,就是针对该缓冲区的**I/O操作**是**基于行**的。 * 在遇到**换行符**前,程序的**输入**和**输出**都会先被**暂存**到**流对应**的缓冲区中 * 而在遇到**换行符**后(或者**缓冲区满了**),程序才会进行真正的**I/O操作**,将该缓冲区中的数据写到对应的**流** (stream) 中**以供后续读取**。 就**标准输入**`stdin`而言,用户的输入首先会被存到**相应的输入缓冲区**中,每当用户按下回车键输入一个**换行符**,程序才会进行I/O操作,将缓冲区暂存的数据写入到`stdin`中,以供[**输入函数**](#c语言中常用的输入函数)使用。 ![stdinBuffer-2022-07-12](https://assets.xbottle.top/img/stdinBuffer-2022-07-12.gif) 而对**标准输出**`stdout`来说,输出内容也首先会被暂存到**相应的输出缓冲区**中,每当输出数据遇到**换行符**时,程序才会将缓冲区中的数据写入`stdout`,继而打印到屏幕上。 <a id="nonImmediatelyPrint"></a> 这也是为什么在缓冲模式下,输出的内容不会立即打印到屏幕上: ```c #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; } ``` 运行效果: ![outputBuffer_remake-2022-06-28](https://assets.xbottle.top/img/outputBuffer_remake-2022-06-28.gif) 可以看到,直到执行到第二个`getchar()`时,屏幕上没有新的输出。 而在执行了`printf("Print!")`之后,输出缓冲区**被填满**了,输出缓冲区中现有的`10`字节的数据被写入到`stdout`中,继而才在屏幕上打印出`123456789P`。 缓冲区内容被读走后,剩余的字符串`rint!`接着被写入输出缓冲区。程序运行结束后,**输出缓冲区**中的内容会被全部打印到屏幕上,所以会在最后看到`rint!`。 ## C语言中常用的输入函数 输入函数做的工作主要是从**文件流**中读取数据,亦可将**读取到的数据**储存到内存中以供后续程序使用。 ### 基于字符 ```c // 从给定的文件流中读一个字符 (fgetc中的 f 的意思即"function") int fgetc( FILE *stream ); // 同fgetc,但是getc的实现*可能*是基于宏的 int getc( FILE *stream ); // 相当于是getc(stdin),从标准输入流读取一个字符 int getchar(void); // 返回获取的字符的ASCII码值,如果到达文件末尾就返回EOF(即返回-1) ``` ### 基于行 ```c // 从给定的文件流中读取(count-1)个字符或者读取直到遇到换行符或者EOF // fgets中的f代表“file”,而s代表“string” char *fgets( char *restrict str, int count, FILE *restrict stream ); // 返回指向字符串的指针或者空指针NULL ``` ### 格式化输入 ```c // 按照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) ``` ## 最常到的输入流问题 先来个不会出问题的示例: ```c #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; } ``` 运行效果: ![correctExample-2022-06-28](https://assets.xbottle.top/img/correctExample-2022-06-28.png) ------ 出问题的示例: ```c #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; } ``` 运行效果: ![incorrectExample-2022-06-28](https://assets.xbottle.top/img/incorrectExample-2022-06-28.png) 因为我将格式设置为了`%3s`,所以`scanf`**最多**接收包含三个字符的字符串。 在这个示例中,我按要求输入了一条字符串`Hello`,并按下**回车**输入一个换行符,缓冲区数据`Hello\n`被写入到了`stdin`中。而`scanf`只从标准流`stdin`中读走了`Hel`这一部分字符串。 此时,标准流`stdin`中实际上还剩3个字符: 1. `l` 2. `o` 3. `\n` (回车输入的换行符) 于是接下来三次**针对字符的**输入函数只会分别**从`stdin`中**取走这三个字符,而**不会等待用户输入**,这就没有达到我想要的效果。 在基本的命令行程序中很容易遇到这类问题,这也是为什么需要及时**清空输入流`stdin`中的数据**。 ## 如何处理残余内容 💡 以下内容假设`stdout`和`stdin`两个标准流都是在**行缓冲**模式下的。 ### 标准输出流stdout 虽然本文主要是写输入流,但这里我还是掠过一下标准输出流`stdout`。C语言标准库中提供了一个**用于刷新输出流缓冲区**的函数: ```c int fflush( FILE *stream ); // 如果成功了,返回0,否则返回EOF(-1) ``` 要清空标准输出流对应的缓冲区,只需要使用`fflush(stdout)`即可。上面的[这个例子](#nonImmediatelyPrint)可以修改成这样: ```c #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; } ``` 运行效果: ![outputBuffer_fflush-2022-06-29](https://assets.xbottle.top/img/outputBuffer_fflush-2022-06-29.gif) 可以看到,加入`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](https://en.cppreference.com/w/c/io/fscanf),也就是说,格式化字符串中的**空白符**(如`"\n"`, `" "`, `"\t\t"`)会吸收**输入字符串**中的**一段连续的空白符**。 也就是说,下面这句格式化输入函数: ```c scanf(" %c %c",&recvChar1,&recvChar2); ``` 可以从`stdin`中读取形如` \n a b`,` \t a b`这样的数据。其中`a`之前的空白符和`a`与`b`之间的空白符都会被吸收,`scanf`得以能准确获取字符`a`和`b`。 依靠这个特性,我们可以在接收输入时自动剔除`stdin`**中残留的空白符**: ```c // 因为格式%s不会匹配多余的空白符,这里按回车后,stdin中会残留一个换行符\n scanf("%s",recvStr); // 在格式%c前加一个空格,可以吸收掉上面残留的换行符\n,程序便能如预期接受用户输入 scanf(" %c",&recvChar); ``` 然而,这一种方法仅只能剔除**多余的空白符**。 #### 使用中括号字符集 这个解决方法可以和上面剔除空白符的方法进行结合。 格式化输入有一个说明符 `%[set]`,它的功能和**正则表达式**中的中括号`[ ]`十分类似: * 其中`set`代表一个**用于匹配的字符集**,一般情况下匹配的是存在字符集中的字符 * 字符集的第一个字符如果是`^`,则表示**取反**,匹配的是**不存在于该字符集中的字符** * 可以在中括号中使用短横线 `-` 来表达**一个范围**,比如`%[0-9]`代表匹配0-9之间的字符。值得注意的是,对于短横线`-`,可能在**不同编译器**之间**有不同实现**,它是[implementation-defined](#关于implementation-defined)的。 另还有一个说明符 `*` ,它被称为**赋值抑制**或**赋值屏蔽**符。如字面意思,在`%`引导的格式转换字串中如果包含`*`,这个格式**匹配**的内容**不会被赋给任何变量**。 ----- 于是,可以给出如下的语句: ```c // 星号 * 代表不会把匹配到的内容赋给变量,相当于“吸收”掉了 // [^\n] 代表除了换行符外一律匹配 scanf("%*[^\n]"); ``` 因为用户结束一次输入的标志通常是按回车输入一个换行符,残留的内容往往**末尾是一个换行符**。上面这句的原理就是**吸收掉`stdin`中所有的残余字符**,直至达到最后一个字符,也就是**换行符**。 然而,换行符不会被上面这句所吸收,所以在接下来的输入中只需要**忽略`stdin`中的残余空白符**即可(换行符就是空白符之一): ```c scanf("%*[^\n]"); scanf(" %c",&recvChar); ``` 这种方法已经可以解决一般情况下的输入残余问题,不过在后续接受格式化输入时还得**忽略换行符**`\n`,还是有点麻烦。 #### 循环取走残余字符 这一种方法能在清除残余时**顺便吸收掉末尾的换行符**`\n`。 取字符需要用到[取单个字符的输入函数](#基于字符),这里为了方便,选用的是`getchar()`。 一般情况下可以这样写: ```c // getchar() 会从 stdin 中取走一个字符 while(getchar() != '\n') ; ``` (使用前提:`stdin`中有残余) `while`循环会一直进行,直至`getchar()`**取到**的字符为换行符`\n`为止,这样就可以顺带吸收掉末尾的换行符了,能相对完美地清除掉`stdin`中的残余内容。 (在**行缓冲**模式下,用户的一次输入通常以一个换行符结束) ------ 不过咧,还可以考虑更周全点。在`getchar()`获取字符**失败**的时候会返回`EOF`,但此时并不满足`while`循环的退出条件,对此可以再完善一下: ```c // 临时储存字符 // 之所以是整型(int),是因为EOF是一个代表 负值整型(通常为-1) 的宏 int tempChar; // tempChar=getchar()这种赋值语句本身的返回值就是所赋的值 while ((tempChar = getchar()) != '\n' && tempChar != EOF) ; ``` 这样一来,当`getchar()`失败时,程序执行就会**跳出循环**。 ------ 综上,针对`stdin`中的残余内容的清除,**最建议采用**的便是最后这种处理方法。 不过其他的方法也是可以在一些场景中使用的,这就见仁见智了... ### 什么时候会返回EOF 这里提一个题外的点:什么时候`getchar()`会返回`EOF`?再进一步想,什么时候程序会认为标准流`stdin`达到了文件流末尾? 实际上,这里的`EOF`往往是用户输入的一个特殊二进制值<sup>[3]</sup>,输入方式: * 在Windows系统下是 <kbd>Ctrl</kdb> + <kbd>Z</kbd>(<kbd>F6</kbd>应该也行) * 在Linux下是 <kbd>Ctrl</kdb> + <kbd>D</kbd> 当用户在输入中发送`EOF`时,标准流`stdin`就会被标记为`EOF`,因此`getchar()`就会获取字符失败而返回`EOF`。 ```c // 测试用代码 #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`语句往往放在输出函数执行完成之后,以立刻将输出内容打印到屏幕上: ```c printf("Hello "); printf("World!\n"); fflush(stdout); ``` 当然,如果嫌麻烦可以**在输出前**直接通过`setbuf`关闭`stdout`的缓冲: ```c setbuf(stdout, NULL); ``` ----- 对于标准输入流`stdin`来说,处理残余的语句往往放在**每次输入函数执行之后**,以及时清理流中残余内容: ```c 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`),千万不能混淆了。 ![sleeping-2022-07-14](https://assets.xbottle.top/img/sleeping-2022-07-14.png) 最后,感谢你看到这里~ 本笔记可能还是有错误出现,也请各位多指教! ## 参考文献 ### 本笔记相关: 1. [File input/output - cppreference.com](https://en.cppreference.com/w/c/io) 2. [Clarify the difference between input/output stream and input/output buffer - StackOverflow](https://stackoverflow.com/questions/51458342/clarify-the-difference-between-input-output-stream-and-input-output-buffer) 3. [End of File in stdin - StackOverflow](https://stackoverflow.com/questions/28216437/end-of-file-in-stdin) ### 关于"implementation-defined" * [Undefined, unspecified and implementation-defined behavior - StackOverflow](https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior) <script>/* bblock.s(); */</script> {(PostContentEnd)} {(PostTag)}学习,小记,C{(PostTagEnd)} {(PostID)}284{(PostIDEnd)} {(PostCover)}none{(PostCoverEnd)} {(PubTime)}1657801746501{(PubTimeEnd)} {(EditTime)}1657801746501{(EditTimeEnd)}
{(PageType)}post.otp.html{(PageTypeEnd)}