首页 C语言番外:标准输入输出到底是什么?
文章
取消

C语言番外:标准输入输出到底是什么?

任何一个学过 C 语言的人都听过标准输入/输出吧,从我们的第一个“Hello World”程序开始,我们就和stdio.h这个头文件结下了不解之缘。std 即是标准的意思,io 即是输入/输出的意思。那么,标准输入/输出到底是什么意思?

在《C 程序设计》(清华大学出版社/谭浩强著第三版)13.1 C 文件概述中有这么一段描述:“以前各章节所用到的输入和输出,都是以终端为对象的,即从终端键盘输入数据,运行结果输出到终端上。从操作系统的角度看,每一个与主机相联的输入输出设备都看作是一个文件。例如,终端键盘是输入文件,显示屏和打印机是输出文件。”

这段话寥寥数字,并没有说清楚什么是输入输出,反而又引入了几个新概念,比如“终端”、“主机”,以及“输入输出设备看作是一个文件”对于初学计算机的人来说都是很难理解的,甚至可能会让一些人理解为键盘即为标准输入,显示器/打印机即为标准输出。

什么是终端

从 1946 年世界上第一台计算机的诞生到今天还差几个月才有 76 年。而个人计算机,大概是上世纪 80 年代起开始被生产出来的,至今也就 40 年。随着科技的快速发展,计算机的价钱已经降到每个人都能买的起的地步,但是在个人电脑还没普及前,计算机还是非常昂贵、非常稀罕的。那时候一台计算机会有很多人共享同一台计算机。

怎么共享的呢,最开始是用一个叫“电传打字机”的设备,每一个需要使用计算机的人都有一台单独的电传打字机,相比昂贵的计算机来说,电传打字机就比较廉价了。下面是一张电传打字机的图片,电传打字机上有键盘作为输入设备,有打印机作为输出设备。在键盘上输入的内容会通过线路传送给真正的计算机,而计算机的输出内容则会通过电传打字机上的打印机打印出来。我们在 C 语言里常用的printf函数“打印”的内容只是显示在了屏幕上,但是如果你是使用的电传打字机连到电脑上,通过电传打字机的键盘运行我们的程序的话,执行结果就会真真正正的“打印”在纸上。

电传打字机

我在写这篇文章的时候,在 B 站上看到一个视频“在30多年前的电传打字机上聊天是怎样一种体验?”。大家可以看一下,非常有意思。可惜公众号文章里是不允许附站外链接,所以我把这个视频的地址做成自动回复了,只需要在本公众号中回复“电传打字机”即可获取视频地址。

刚才提到的这个电传打字机就是所谓的“终端”,电传打字机实际上是一个键盘和一个打字机的组合,它自己内部并没有处理器。再后来,随着科技的进步,终端也更高级了,变成了屏幕和键盘,如下图是 DEC 公司生产的 VT05 型号的终端。

VT05

而现在,个人电脑已经普及,甚至我们现在使用的手机的计算能力都超过当年所谓的大型机,终端这种设备也早已退出历史舞台。现在我们可以广泛的把终端理解为输入设备和输出设备的组合。不过,其实现在的电脑里也依然还有“终端”这个东西,只不过它从一个物理设备变成了一个虚拟程序,这就是我们系统中的命令行窗口了(目前 macOS 上的命令行程序的名称还是叫“终端”)。

另外,我们在 C 语言的一些章节还经常看到“控制台”这个词,这又是什么鬼呢?在早期的大型机上,确实会配一个工作台,上面有显示器,有键盘,会有一个管理员坐在这个工作台上对计算机进行管理,控制台也是一个终端,只不过到如今,终端和控制台都从看得见的物理设备变成了看不见的虚拟程序。

标准输入输出

终端和主机的概念能理解了,那“从操作系统的角度看,每一个与主机相联的输入输出设备都看作是一个文件。例如,终端键盘是输入文件,显示器和打印机是输出文件。”也就好理解了。不管你是通过“电传打字机”还是通过“带屏幕的终端”在使用计算机,对于操作系统来说是没有区别的。如果你使用的终端的输出设备是显示器,那么通过printf打印出来的字符会显示在显示器上,如果你使用的终端的输出设备为“打字机”,那么通过printf打印的字符便会被打字机打印在纸上。显示器和打印机在操作系统上都是一个文件,并且这个文件是“可写”的,操作系统只需要往这个文件中写入内容,电传打字机和屏幕自动就会打印或显示内容了;同理,输入设备在操作系统上也是一个文件,但这个文件是只读的,操作系统通过读取文件的操作来获取我们通过键盘输入的内容。

讲了够多和 C 语言无关的东西了,现在终于可以回到 C 语言的标准输入输出了。打开stdio.h文件,可以找到三个宏定义:

1
2
3
4
5
6
7
8
9
extern FILE *__stdinp;
extern FILE *__stdoutp;
extern FILE *__stderrp;

// ...

#define	stdin	__stdinp
#define	stdout	__stdoutp
#define	stderr	__stderrp

stdin、stdout、stderr 分别就是标准输入、标准输出、标准错误,我们可以看到这三个定义都是FILE *文件类型的。默认情况下,stdin 会绑定到终端的输入设备(键盘)上;stdout、stderr 会绑定到终端的输出设备上,也就是在命令行窗口输出。

输入、输出重定向

既然对于操作系统来说,输入输出设备都是文件,那么当然用普通的文件作为输入输出设备也是可以的,这就是输入、输出重定向。

输入重定向

举个例子,比如我们写了一个“算命”程序,这个程序通过scanf从标准输入读取一个人的姓名、性别、出生年月日等信息,再结合《易经》中的讲的宇宙规律计算出这个人的“吉凶祸福”。我们可以再做一个问卷调查网页用来收集用户的信息,当一个用户提交了自己的信息后,你当然可以手动通过键盘再把他的信息输入到我们的程序中,前提是你需要时时刻刻等着,一有用户提交信息,你就手动运行一次算命程序,即使这样,当用户数量达到一定数量时,你敲键盘的速度就跟不上了。记住:作为一个程序员,能让计算机解决的问题,绝不自己动手,包括敲键盘。

不然怎么办呢?我们可以把用户提交的数据保存到一个文件里,然后把标准输入重定向为这个文件。比如,一个叫张三的用户的信息到一个名为 zhangsan.txt 的文件中,文件内容如下:

1
张三 男 2000 01 01

我们的代码如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main() {
    char name[9], gender[3];
    int year, month, day;
    scanf("%s %s %4d %2d %2d", name, gender, &year, &month, &day);
    // 根据易经计算命运的代码省略
    printf("姓名:%s 性别:%s 年龄:%d \n运势:略\n", name, gender, 2022-year);
    return 0;
}

程序调用及输出如下:

1
2
3
$ ./yijing < zhangsan.txt
姓名:张三 性别:男 年龄:22 
运势:略

yijing 是我们这个程序编译后可执行程序的文件名,<在这里代表输入重定向,它的作用是把zhangsan.txt这个文件的内容作为标准输入,这样对于我们的程序来说,标准输入就变成了zhangsan.txt,而不再是键盘了。

输出重定向

标准输出有两个,一个是 stdout,一个是 stderr。那么有个问题,为什么要有两个,一个不就行了吗?只有一个当然可以,但是你想象一个场景,我们写了一个程序,我们这个程序在运行的过程中会打印一些内容(打印的东西也叫日志),这些日志中既有正常运行时打印的运行日志,也有程序在遇到问题时打印的错误日志,这些日志都混在一个标准输出中。当程序产生错误时,我们打开“标准输出”,在其中查找致使程序产生错误的原因,发现标准输出中绝大部分日志内容都是正常运行产生的日志,只有极少是错误日志。但是只有错误日志对帮我们查找错误原因有帮助,如果我们的程序是7*24小时运行的(例如服务器程序),在里面找错误日志无异于大海捞针。

所以,我们要把正常运行时的打印和产生错误时的打印分开,这就是为什么要有 stderr 的原因。那么,我们该怎么往 stderr 里输出内容呢?前面讲过 stderr 是文件类型的,所以和向文件中输出内容是一样的,stdio.h中声明的可以输出到标准错误的函数:

1
2
fprintf(stderr, "%s", "除数不能为 0。"); // 此函数用法类似于 printf
perror("除数不能为 0。");

用上面这两个函数都可以输出内容到 stderr,当然其他文件输出函数也是可以的。

默认情况下,stdout 和 stderr 都是终端的输出设备,也就是输出到命令行窗口显示。在很多情况下,我们是不希望将输出内容显示到命令行窗口上的,比如我们的程序是定时执行的(操作系统允许设置定时任务来定时执行一些程序),到了指定时间后,操作系统自动运行我们的程序,如果程序运行时,我们不在电脑跟前,那就看不到程序输出的内容。比如,我们的程序很长时间,我们不可能时时刻刻盯着终端的输出。这些情况下,我们就用到了输出重定向,输出重定向可以把 stdout 和 stderr 重定向到文件中,这样通过printf等函数打印的内容就不是显示在屏幕上了,而是保存在我们指定的文件中。

还是写个示例:

1
2
3
4
5
6
7
#include <stdio.h>

int main() {
    printf("正常运行产生的日志内容\n");
    fprintf(stderr, "程序遇到问题输出的日志内容\n");
    return 0;
}

这个程序编译后的可执行文件命名为 demo,以下是运行 demo 程序的命令:

1
./demo 1>demo.log 2>error.log

1>demo.log代表把 stdout 重定向到名为demo.log的文件中,2>error.log代表把 stderr 重定向到名为error.log的文件中。其中的 1 和 2 分别代表 stdout 和 stderr,>代表输出重定向,运行这个程序后,如果当前目录没有名为 demo.log 和 error.log 的文件则会自动创建,如果当前目录下这两个文件已存在,文件中原来的内容则会被覆盖。运行程序后,demo.log 文件的内容为:

1
正常运行产生的日志内容

error.log 文件的内容为:

1
程序遇到问题输出的日志内容

好了,这一期就讲到这里,这好像是到目前为止最长的一篇,能看完的都是真爱粉了。感谢关注,后续我也会继续努力通过本公众号向大家输出更多的干货。

下期预告:什么是头文件?

本文由作者按照 CC BY 4.0 进行授权

C语言番外:如何给main函数传参?

C语言番外:在C语言里怎么连接数据库?