0 说在前面

当你看到这篇文章时,不妨回想一下你当初第一次用 C 语言“Hello World!”时是什么样的心情。那是你第一次成功使用神秘代码完成了和计算机的交流。尽管展示信息的黑框框让你可能不大习惯这样一种沟通方式,但这难道不也有点电影里黑客那感觉了~?

不知不觉半年过去了,你为了数据结构作业绞尽脑汁,敲下最后一个分号,鼠标轻点”编译运行“。黑色高级框框跳出来,尴尬而不失礼貌地对你说:


--------------------------------
Process exited after 4.511 seconds with return value 3221225477
请按任意键继续. . .

你质问:

”你怎么了,为什么要这样对我……呜呜~“

是啊,你认识了这个框框那么久,它早已熟悉你写 bug 的习惯,而你却摸不清它的性情。你是时候应该了解一下它了。

1 标准输入输出

”你好,我叫终端,也叫控制台,英文名是 Terminal,也叫 Console,很高兴成为你的朋友。“

”你不记得我啦?我就是你每次运行程序的时候跳出来跟你聊天的那位。请看——“

一文看懂 C 语言 I/O-LMLPHP

“其实我并不是你的程序本体,你的程序躲在电脑里面,是它派我来跟你说话的,”

“当你在键盘上敲敲的时候,我会帮你把你输入的字符显示出来,这样你就知道你输入的对不对了,”

“然后你一行输完,按下回车,我帮你把整行字符串都传给你的程序,你的程序就会对一行字符串进行解析,如果有 scanf 函数的话还会逐个解析出里面的数字、字符等等,”

“当你的程序算完之后,会把输出的信息告诉我,我来显示到屏幕上。”


所谓 I/O,就是 Input/Output,即输入输出。通过终端读入和显示的就是“标准输入输出”,由于终端也是从键盘获取信息,并把信息显示在屏幕上。所以:

  • 标准输入也叫键盘输入
  • 标准输出也叫屏幕输出

标准输入输出的英文是 Standard Input and Output,缩写就是“stdio”,觉不觉得眼熟hhh~


——“你用的 scanf、gets、getchar 函数都是解析标准输入的,printf、puts、putchar 都是标准输出。现在知道我是干什么的了吧”

——“哦,原来是这样。但是你好丑。”

——“???那我走”

2 输出输出重定向

终端走了——你万念俱灰,把你的代码提交给希冀的评测姬。她说:

得分0.00   最后一次提交时间:2022-03-25 19:29:50

共有测试数据:5
平均占用内存:1.401K    平均CPU时间:0.00578S    平均墙钟时间:0.00576S

测试数据	评判结果
测试数据1	运行错误
测试数据2	运行错误
测试数据3	运行错误
测试数据4	运行错误
测试数据5	运行错误

——“求求你在本地测好再交给我 OK?我每天判那么多代码很累了啦!”

——“emm……我好奇你怎么知道我们的代码对不对的,也是用终端吗?”

——“终端?那不是低级的 PC 才会用的东西?我们服务器不需要这个。I/O 重定向一下就行了”


现在你可以试试这样一个操作,写好一份 C 语言代码,里面有标准输入输出函数,然后添加两行这样的语句:

#include <stdio.h>
// 一些额外的头文件和宏定义
int main() {
    freopen("a.in", "r", stdin);
    freopen("a.out", "w", stdout);   // 额外添加这两句 :)
    ...
    return 0;
}

然后在你的 C 程序的同一文件夹下新建文本文档,命名为 “a.in”(注意这一步之前要确保你的电脑显示了文件后缀)。然后在 “a.in” 里面写上你要输入的数据,Ctrl+S 保存。

编译运行你的代码,你会发现程序直接结束,黑框框没有其它输出了。

然后你在代码所在的文件夹里发现了一个名为 “a.out” 的文本文档,里面正是你要的答案。

当然 “a.in” 和 “a.out” 可以改成你喜欢的任何名字,文本文档对后缀不敏感,跟 “.txt” 是一样的。

当然你可以像我一样玩(用编辑器打开输入文件分屏出去,调试的时候不用每次在控制台输入,多是一件美事):

一文看懂 C 语言 I/O-LMLPHP

回过头来我们看看这两句是什么意思:

freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
/*
 * freopen: f    表示 “file (文件)”
 *          re   表示 “重新”
 *          open 表示 “打开”
 *
 * "a.in" / "a.out" 表示重定向的文件名
 * "r" / "w" 表示文件的打开模式:"r" 意味着“读”,"w" 意味着写
 * stdin / stdout 表示被替换的 I/O 方式
 *                分别是标准输入(standard input)和标准输出(standart output)
 */

翻译成人话就是:

  • 我要用“只读方式”打开文件 “a.in”,并用其替换标准输入
  • 我要以“只写方式”打开文件 “a.out”,并用其替换标准输出
    • 顺便说一句,只写模式 “w” 下,如果找不到文件,程序会帮你创建一个~

评测姬说:

“现在你懂了?在我拿到你的程序时,会自动帮你加上 freopen 将标准 I/O 重定向为文件 I/O,再在我的 CPU 里跑程序,跑完再对比一下你的输出和标准答案一不一样就行了。”

3 文件 I/O

此时晏老师:“多出点文件 I/O 的题,难死这帮小崽子~”

理论上来说,你会 I/O 重定向之后就可以做所有文件 I/O 的题了,大不了都用 scanf 和 gets 呗。

但是有时候让你既从标准输入读入又从文件读入~文件 I/O 也不能不会是吧。

废话了这么多,终于可以讲讲你们不大清楚的 I/O 函数的用法了:

3.1 文件指针

要熟练使用文件 I/O,要过的第一关就是文件指针,它相当于给你的文件贴一个标签,让后当你需要调用函数的时候要把文件指针作为输入变量传进去,这样才能对你的文件进行操作:

FILE * file_in = fopen("in.txt", "r");
FILE * file_out = fopen("out.txt", "w");
/*
 * FILE * 是一个变量类型,代表文件的指针
 *
 * 后面的 file_in 和 file_out 是你自己起的变量名
 *
 * fopen("...", "r/w"); 是打开文件的函数,前面的文件名,后面是打开模式读或写
 *                      表示将一个文件以某种方式打开,返回该文件的指针
 *
 * 以后你就可以把 file_in 或 file_out 传进其它函数里了
 */

3.2 I/O 函数

3.2.1 输入函数:

scanf("...", ...);
fscanf(file, "...", ...); // file 是前面用 "r" 模式打开的文件指针

在以下两个条件下,这两个函数是我最推荐大家使用的。

  • 需要从输入中获取数字(直接 %d 或 %lf)
  • 需要逐词对字符串处理(不含空格)

如果题不是要求类似于“读入若干行,行内有空格,对每行输出一个balabala……”这种,真心不建议用 gets 和fgets。因为 gets 很可能会产生莫名其妙的 bug(我曾解释过),fgets 不好记也不好用。

所以比如“单词统计”等等这类题,只要不怕空格,还请选择 scanf/fscanf


printf("...", ...);
fprintf(file, "...", ...);

这俩大家应该挺熟了,后面那个 fprintf 就是把输出目标换成 “w” 模式的文件指针就行了。

介绍两个新朋友:

len = fread(str, sizeof(str[0]), MAX, file);
/*
 * 这个函数的作用是从 "r" 模式的 file 文件里把整个文件一股脑读到 str 里
 *
 * str  是要接受的字符串,尽量开大点,一定要初始化为全 0,这个函数不保证在字符串末尾补 '\0'
 * sizeof(str[0]) 实际上就是一个字符的大小,表示读的单位大小
 * MAX  读的最大长度,尽量跟 str[] 的容量一样大,要大于所给数据范围
 *      如果读到文件末尾还不到 MAX 则返回 str 的长度
 *      如果读到 MAX 则返回 MAX
 * file 文件指针
 */

fwrite(str, sizeof(str[0]), len, file);
/*
 * 这个函数的作用是把 str 一股脑写进 "w" 模式的 file 里
 *
 * str  是要写的字符串
 * sizeof(str[0]) 解释同上
 * len  是想写的长度,也就是 str 的长度
 * file 想写的文件指针
 */

文件加密一题中,需要读取一整段文本,这种情况下,用这两个函数是最好的选择。


剩下的不太常用我大概说一下。

gets(str);              // 读到换行就停止,读进来的字符串不含换行,可能引起神秘 bug
fgets(str, MAX, file);  /* 读到换行 / 文件末尾 / 超过 MAX - 1 时停止读入
                         * 特性:str 中保留读到的换行并自动在末尾添加 '\0' */

上面这俩函数如果想用的话还是建议好好研究一下特性小心一点使用,挺容易出 bug 的。

ch = getchar();         // 从标准输入读入单个字符,毒瘤,别用
                        // 建议想用的时候用 scanf("%s", str); 读字符串来避免 bug

ch = fgetc(file)        // 从文件中读单个字符,注意的一点是:
                        // 请把 ch 定义成 int 类型,因为它读到文件末尾会返回 EOF
                        // 而 char 类型不能储存 -1 导致无法识别文件末尾
putchar(ch);            // 向标准输出写一个字符,等同于 printf("%c", ch);
fputc(ch, file);        // 向文件写一个 ch,等同于 fprintf(file, "%c", ch);


03-25 23:04