成事不足,败事有余

· · 算法·理论

::::info[声明] 作者其实也只是一知半解。因此本文中的内容仅供参考,请勿将其作为严谨的学术内容看待。欢迎指出文中的错误,这对所有人都是好事。 ::::

引入

在 NOI 系列比赛中,文件 I/O 是必须学会的东西。

freopen("XXX.in","r",stdin);
freopen("XXX.out","w",stdout);

如果你不会写这个,并且不会别的文件 I/O 方式,想必你一定能取得一个“好成绩”。

而众所周知,cin/cout的速度非常慢,有时候可能会带来美妙的 TLE。所以,我们经常会解绑cin/cout,并且关闭 C/C++ 风格 I/O 的同步流,以提升 I/O 效率。

ios::sync_with_stdio(false);
cin.tie(nullptr);

最后,为了保险起见,有人还会加上最后两句。

fclose(stdin);
fclose(stdout);

那么想必这样的代码一定能取得好成绩吧!

转瞬即逝

是的,你忐忑地点开成绩查询页面,一看到成绩,悬着的心终于死了。

0

你随即打算以 9.8\text{m/s}^2 的加速度去楼下买辣条。不过在你做出这个举动之前,你想到测测自己的程序,于是你把它提交到某国内著名 OJ 上一测——这不 AC 了吗?

这里面似乎藏着更多。我们应该看看,究竟是什么导致了爆零的悲剧。

Hello,World

我们从最简单的程序——Hello,World 开始。

一个很正常的程序。猜猜输出文件里有什么?

什么都没有。

没错,输出文件里居然什么都没有!我们可以写几个不同的程序,但最后似乎总是没有输出。

爆零的原因已经知道了,就是输出的问题。那么,是什么造成了这种情况呢?显然要么是解绑和关同步流的问题,要么是fclose()的问题,毕竟你也不能怀疑到freopen()头上。我们试着把解绑的代码注掉重新运行,发现依旧没有输出。但当我们把关同步流或者fclose()中的任意一个注释掉,输出就成功了!

究竟是怎么一回事呢?

缓冲区

C++/C 流的缓冲区关系取决于是否关闭了同步流。

缓冲区是啥?它是内存中的一块临时存储区域,通过在数据传输过程中存储一定信息,减少调用效率低下的设备的次数,来优化运行速度。

举个例子,假如你是一个快递员,如果你每接到一个包裹就去送,来来往往的,非常浪费时间。于是你选择把包裹先送到快递站,等到快递站塞满了或者遇到特殊情况再去送,效率就高了不少。把全站的包裹全部拿去送的操作,我们称之为“刷新”。

默认情况下,C++/C 流是用同一个缓冲区的,这样就不会在混用时让顺序错乱。但这样做速度跟不上,于是 OIer 的大手发力了!他们关闭了同步流,现在 C++ 流拥有了独立的缓冲区。这下 C++ 流的速度就更快了,当然这也意味着不能混用它们了。

不过就算关闭了同步,C++/C 流仍然共用同一个文件描述符最终完成输出。

悲剧复盘

接下来我们来复盘悲剧是如何发生的:

  1. 通过freopen()重定向到文件;
  2. 关闭同步流,于是写入Hello,World到 C++ 流的缓冲区;
  3. 通过fclose()刷新 C 流的缓冲区,并关闭文件描述符
  4. 程序结束,C++ 流的缓冲区刷新,但由于文件描述符已经关闭,它的输出失败,缓冲区里的东西也就被直接丢弃了。

这就是为什么没有输出。如果没有关闭文件描述符这一步,C++ 流就能成功刷新并输出。

至于没有关闭同步流的情况,这是由于在同步流开启的情况下,C++/C 流共享一个缓冲区,在fclose()这一步后就成功输出了。要是关闭了同步流,C++ 流用上了独立的缓冲区,fclose()就管不到它了,并且由于fclose()关闭了文件描述符,之后的刷新就无能为力了。

成事不足,败事有余。

解决方案

很简单,不要手滑写fclose()。当然,如果你改不掉这个习惯,你应该在fclose()之前进行手动刷新操作。

cout.flush();
cout<<fulsh;
cout<<endl;

这些都是可以的,它们会刷新 C++ 流的缓冲区。不过最好还是不要写fclose()

后记

现在你终于知道了为什么会爆零!你斗志昂扬,决定下次一雪前耻。

不过还是先去买辣条吧。