Loading... # 通俗解释流的概念 > **本文为**[**【程序开发科普系列】**](https://blog.zsh2517.com/archives/30/)**内文章** ## 0x00 写在前面 **为什么要写这个?** > Q: 你看看这个题,怎样把数据全都进去然后处理? > A: 为什么要全都读进去? > Q: 看这个输入输出,输入是连着的,然后才统一输出... > 因为经常有人问我比如某个题目,输出是在一块的(输入一个区域,输出一个区域),怎么才能全读进去/是不是必须全都读进去啊? 如果上面的对话没看明白,看一下比如一个例题 > **题目描述** > 第一行输入一个$n$,是数据的组数 > 之后每行输入两个数$a_i$, $b_i$ ($1\leq i\leq n$) > 输出每行两个数的和 > **输入** > 2 > 1 4 > 2 8 > **输出** > 5 > 10 > **题干不是这么给的输入输出 (`→|` 输入 `←|` 输出)** > →|3 > →|1 4 > ←|5 > →|2 8 > ←|10 > 那么能不能写一个程序,读入组数之后,每次读入两个数,然后输出一个结果,读入两个数,然后输出一个结果。类似于下面这样 > 而不是把`2 1 4 2 8`全部存下来再运算一块输出`5 10`呢? **说明** 由于这个是面向**初学编程**的,因此尽量减少了专业性,同时避免了一些特殊情况的出现,如果有不准确的还望指正。 ## 0x10 什么是流 简单来说是有顺序的一串字节的序列。这串序列可以连续地读写。 (类似于磁带,只能按顺序播放,否则就要倒带等等) ## 0x20 输入流与输出流 在学过的C/C++/python(视情况)中,先不考虑文件读写,那么一个程序的所有的交互就是通过**键盘输入**和**屏幕显示**这两块实现的。 例如 C 语言的 `printf` `scanf`,C++ 的 `cin` `cout`,python 的 `input` `print` 实现的。(后面的例子均提供C语言和python的,C++参考C语言) 这时如果把程序看作一个黑匣子(不考虑程序的实际功能),那么可以认为是下面的结构 ```plaintext | | ============== | | ============== 键盘输入 -> | 程序 | -> 屏幕输出 ============== | | ============== | | ``` 即对于我们的程序来说,只有一个输入,一个输出口。程序从输入口读进去,然后从输出口写(输出)出来。 无论是键盘输入的数据,还是程序输出的数据,本质都是一串字节序列。即上面提到的“流”。 **记住这个图形,输入和输出不同的口(或者说是两个不同的方向)** ## 0x30 两个口是分离的 也就是说,输出和输入互不干扰。而我们看到的那个黑框,只是提供了一个交互的环境,即上面的图形可以变成 键盘输入 -> 黑框 -> 输入流 -> 程序 屏幕 <- 黑框 <- 输出流 <- 程序 这个黑框帮我们把输入和输出混合到了一起。形成了一种交互式的感觉。 输入输出二者本身就是分离的,这个也就是为什么OJ(在线评测系统)可以把输入输出分离开写的原因。 当然部分题目/教材,为了方便理解,可能会采取比如下划线、标注等等,将输入输出写在一块,这样也没问题。 <small>交互?也就是你输入一些东西,然后它有一个反映</small> ## 0x40 缓冲区 这里首先举个例子 ```cpp #include <stdio.h> int main(){ int x, y, i; printf("请输入一个整数: \n"); scanf("%d", &x); printf("你输入的是%d\n", x); printf("请输入另外一个数字,虽然可能光标未显示,但是你相信你输进去了,然后按回车\n"); for(i = 0; i < 2000000000; i++){ // 仅仅一个延时的效果 // 具体根据自己电脑运行速度调整循环的数字 } printf("现在循环结束了,如果你还没输入,那么重新开始一下\n"); scanf("%d", &y); printf("你输入的是%d\n", y); return 0; } ``` 首先输入一个数字,然后会立刻显示出来我们输入的东西。之后,显示让我们输入第二个数字的时候,按下比如 `34` `Enter`,屏幕上可能没有回显(如果有提示已经循环结束的话,那么调大这个循环次数) 等一会,循环结束之后,会显示输入的内容,和应该的输出。 所以... emm... 虽然没显示出来,但是这个现象表明,数字已经输入进去了,只是我们的程序还没接收到而已。 因此很容易想到,这里面应该是有一个临时存储的区域 这个区域实际就是缓冲区 ## 0x50 缓冲区2 简单来说,是为了提高程序的效率。 比如下面的代码片段 ```cpp scanf("%d", &a); printf("%d\n", a); scanf("%d", &b); printf("%d\n", b); ``` 正常来说应该是这样输入输出 ```plaintext →|1234<Enter> ←|1234 →|2234<Enter> ←|2234 ``` 但是如果输入`1234 2234 <Enter>`呢? ```plaintext →|1234 2234<Enter> ←|1234 ←|2234 ``` 会变成这样,也就是第一个输入的 `1234` 被读进去了,而第二个输入的 `2234` 没有被读入。等到下次需要读入的时候,才继续从缓冲区的部分继续读入。(等到缓冲区没有可以读入的时候,等待用户输入) 如果换成这样 ```cpp scanf("%d", &a); printf("%d\n", a); fflush(stdin); scanf("%d", &b); printf("%d\n", b); ``` 里面的`fflush(stdin);`是刷新标准输入缓冲区(即清空) 之后再尝试就会像这样 ```plaintext →|1234 2234<Enter> ←|1234 |<等待输入> ``` ## 0x60 为什么要有缓冲区 一方面是方便使用。首先在有缓冲区的情况下,完全不影响交互式地操作。而除此之外,还额外地支持了一次性输入数据,然后让程序自己运行。 另一方面,在后面的重定向部分,还有着更多的考虑。 ## 0x70 重定向 ### 0x71 准备工作 > 前面提到了,对于程序,输入流和输出流就像两个管子一样,一边进去一边出去。如果两个管子都是指向了那个黑框,则是从键盘获取输入然后输出到屏幕上。 > 那么如果把管子接到别的地方会发生什么? 重定向,就像字面所说的那样,把输入/输出流,重新定向到某个位置(可以是一个程序,可以是另外一个蓝框(实际上属于前面的程序类型),可以是文件,甚至可以是网络和打印机) 首先写一个简单的 `A+B problem` (输入整数 A B,然后输出 A + B 的值,不需考虑数据范围等等问题) ```cpp #include <stdio.h> int main(){ int a, b; scanf("%d %d", &a, &b); printf("%d\n", a + b); return 0; } ``` 这个时候,在放着程序的文件夹里面,按着 `Shift` 右键,应该是有个 `在此处打开 powershell/命令提示符/cmd` 的东西。如果是`powershell` 那么进去之后输入 `cmd` 打开 cmd 操作。 假设我们的程序叫做 a.exe 首先输入 a.exe ```plaintext C:\Users\zsh2517\projects\blog>a.exe 12 23 35 ``` ### 0x72 输入重定向 现在假如有一个 `input.txt` ,里面存放的是 `12 23` 这个数据,怎么办? ```plaintext C:\Users\zsh2517\projects\blog>a.exe < input.txt 35 ``` 这个 `<` 就是把后面的内容(字节集/字节的流),输出给前面的程序(重定向到前面程序的标准输入流) ### 0x73 输出重定向 如果要输出到 `output.txt` 呢? ```plaintext C:\Users\zsh2517\projects\blog>a.exe > output.txt 12 23 ``` 运行之后等待输入,输入完了什么也没显示就结束了。然后打开文件夹下面的 `output.txt` 发现 35 在文件里面。 ### 0x74 二者可以结合使用 ```plaintext C:\Users\zsh2517\projects\blog>a.exe < input.txt > output.txt ``` 就这样一行,然后就可以把 `input.txt` 的内容重定向到 `a.exe` 的标准输入里面,然后把 `a.exe` 的标准输出重定向到 `output.txt` 在某种意义上实现了文件读写 ### 0x75 输出的追加和覆盖 运行两次 `a.exe > output.txt`,每次结束后看一下里面的内容 ```plaintext C:\Users\zsh2517\projects\blog>a.exe > output.txt 1 2 看一下 output.txt 的内容,是 3 C:\Users\zsh2517\projects\blog>a.exe > output.txt 2 3 看一下 output.txt 的内容,是 5 ``` 可以看到,第一次结束后, `output.txt` 是3,然后第二次结束之后覆盖了。 **如果不想让他覆盖呢?** `>>` 是文本追加,即输出到文件的下一行。 删掉 `output.txt`,之后再试一次,使用`a.exe >> output.txt` ``` C:\Users\zsh2517\projects\blog>a.exe >> output.txt 1 2 C:\Users\zsh2517\projects\blog>a.exe >> output.txt 2 3 ``` 文件的内容是 ``` 3 5 ``` 这次对了。 ### 0x76 管道重定向 管道重定向,就是在两个程序之间搭一个管道。从第一个程序的标准输出 连接到 第二个程序的标准输入 例如 写一个 `generate.exe` 是 输出两个数字的,然后写一个 `solve.exe` 是求解 A+B 的 ```cpp // generate.cpp #include <stdio.h> int main(){ printf("2 3"); return 0; } // solve.cpp #include <stdio.h> int main(){ int a, b; scanf("%d %d", &a, &b); printf("%d", a + b); return 0; } ``` 之后运行 `generate.exe | solve.exe` ``` C:\Users\zsh2517\projects\blog>generate.exe | solve.exe 5 ``` 可以看到,`generate.exe` 的输出的 `2 3` 输入给了 `solve.exe` ,然后 `solve.exe` 从这里面读取了输入,之后输出了 `5` ### 0x77 输入重定向会影响键盘输入 也就是比如 `a.exe < input.txt` 的时候,所有输入必须要从 `input.txt` 里面输入,而黑框内的输入是无效的 ## 0x80 错误流 ### 0x81 错误流是啥 > ~~配合重定向食用更好~~ 前面提到了,输入和输出都可以重定向到文件或者其他位置。这样的话,如果程序出错了怎么办?从大量的输出里面找要找的错误信息? 这个时候,就是错误流的使用了,错误流的方向也是输出。即原来的那个图,改成了这样 ```plaintext 标准错误流 ↗ 标准输入流 ----> 程序 ↘ 标准输出流 ``` 在这种情况下,标准输入流和标准输出流都是重定向到了其他地方,但是还有一个标准错误流可以留在屏幕上,这样输入和输出不会受到影响,而错误消息会立即打印到屏幕上方便反应。 ### 0x82 标准输出流重定向不影响错误流 之后配合上上面的重定向,这里首先向两个流去写入东西。 ```cpp #include <stdio.h> int main(){ fprintf(stdout, "这是一条标准输出流的信息\n"); fprintf(stderr, "这是一条标准错误流的信息\n"); fprintf(stdout, "这是又一条标准输出流的信息\n"); fprintf(stderr, "这是又一条标准错误流的信息\n"); } ``` 运行输出是 ``` C:\Users\zsh2517\projects\blog>a.exe 这是一条标准输出流的信息 这是一条标准错误流的信息 这是又一条标准输出流的信息 这是又一条标准错误流的信息 ``` 没有任何问题,就是按顺序输出的。 之后假如叫做 `a.exe`,尝试使用 `a.exe > output.txt` ``` C:\Users\zsh2517\projects\blog>a.exe > output.txt 这是一条标准错误流的信息 这是又一条标准错误流的信息 ``` 发现通过 `stderr` 输出的内容(输出到标准错误流的内容)依然打印到了屏幕上 而输出到 `stdout` 的内容(标准输出流)输出到了 `output.txt` ### 0x83 错误流重定向 那么怎么重定向错误流呢? 方法是 `2>` (即在 `>` 前面加上一个 `2`) 案例,还是上面的代码 **重定向标准错误流** ```plaintext C:\Users\zsh2517\projects\blog>a.exe 2> output.txt 这是一条标准输出流的信息 这是又一条标准输出流的信息 ``` **重定向标准输出流** 这里可以直接使用 `>` ,也可以使用 `2>` ```plaintext C:\Users\zsh2517\projects\blog>a.exe 1> output.txt 这是一条标准错误流的信息 这是又一条标准错误流的信息 ``` **重定向到不同文件** 控制台 ```plaintext C:\Users\zsh2517\projects\blog>a.exe 1> stdout.txt 2> stderr.txt ``` stdout.txt ```plaintext 这是一条标准输出流的信息 这是又一条标准输出流的信息 ``` stderr.txt ```plaintext 这是一条标准错误流的信息 这是又一条标准错误流的信息 ``` 可以看到这两个是分开了的。 ## 0x90 控制台 流的概念已经完了,现在来说一下那个“黑框”(其实也叫比如 `控制台` 等等) 使用过 `Visual Studio` 或者比如 `Codeblocks` 的人,应该对 `Console`, `Terminal`, `控制台`, `终端` 这些词不陌生。比如 `codeblocks` 直接创建的是 `Console` 程序,VS创建项目一开始都是选择 `控制台程序`。(还是不考虑文件输入输出的情况下)选择这些类别写的程序,自己写的东西就是从标准输入流中读取,然后写出到输出流中。 双击运行一个 `控制台程序/黑框的程序/命令行程序`,然后在win7及以上的任务管理器(实际上是 `process explorer`,自带的任务管理器可能不支持)的进程/详细信息 列表中,除了多了 你的程序.exe ,还有一个进程是 conhost.exe (C:\Windows\System32\conhost.exe) <small>这个可以不了解</small> 实际上,负责显示、接受输入输出的是那个 `conhost.exe`。这个程序与你的程序建立管道连接,接收输入、处理缓冲区等等 可以认为这个 `conhost.exe` 实际上就是接管了你的程序的输入流和输出流。将你在 `conhost.exe` 上的操作写入标准输入流,将你的程序的输出到标准输出流的程序。而你写的只是一个没有窗口的程序,然后Windows为你的程序提供了一个默认的窗口。 <!-- 例如 VSCode,以及很多的第三方 Terminal 终端程序也是这么干的,接管了输入和输出流,这样就可以在自己的程序内部,这个放图吧 --> # 0xA0 控制台转义字符 控制台如果 安排 通俗解释流的概念 0x00 写在前面 0x10 什么是流 0x20 输入流与输出流 0x30 两个口是分离的 0x40 缓冲区 0x50 缓冲区2 0x60 为什么要有缓冲区 0x70 重定向 0x80 标准错误流 0x90 控制台(黑框) 0xA0 控制台转义字符 --- ---------上面的写完了--------------- 0xB0 [扩展]控制台(比如VSC等等的第三方工具) 0xC0 [扩展]缓冲区(stderr stdout的区别,还有一个clog,是否立即写入等等) 0xD0 [扩展]简单的Windows设备文件(比如 con nul 等等) 0xE0 [扩展]文件IO中的流 最后修改:2021 年 09 月 19 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏