成事不足,败事有余
CR400BF_1145 · · 算法·理论
::::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);
那么想必这样的代码一定能取得好成绩吧!
转瞬即逝
是的,你忐忑地点开成绩查询页面,一看到成绩,悬着的心终于死了。
你随即打算以
这里面似乎藏着更多。我们应该看看,究竟是什么导致了爆零的悲剧。
Hello,World
我们从最简单的程序——Hello,World 开始。
一个很正常的程序。猜猜输出文件里有什么?
什么都没有。
没错,输出文件里居然什么都没有!我们可以写几个不同的程序,但最后似乎总是没有输出。
爆零的原因已经知道了,就是输出的问题。那么,是什么造成了这种情况呢?显然要么是解绑和关同步流的问题,要么是fclose()的问题,毕竟你也不能怀疑到freopen()头上。我们试着把解绑的代码注掉重新运行,发现依旧没有输出。但当我们把关同步流或者fclose()中的任意一个注释掉,输出就成功了!
究竟是怎么一回事呢?
缓冲区
C++/C 流的缓冲区关系取决于是否关闭了同步流。
缓冲区是啥?它是内存中的一块临时存储区域,通过在数据传输过程中存储一定信息,减少调用效率低下的设备的次数,来优化运行速度。
举个例子,假如你是一个快递员,如果你每接到一个包裹就去送,来来往往的,非常浪费时间。于是你选择把包裹先送到快递站,等到快递站塞满了或者遇到特殊情况再去送,效率就高了不少。把全站的包裹全部拿去送的操作,我们称之为“刷新”。
默认情况下,C++/C 流是用同一个缓冲区的,这样就不会在混用时让顺序错乱。但这样做速度跟不上,于是 OIer 的大手发力了!他们关闭了同步流,现在 C++ 流拥有了独立的缓冲区。这下 C++ 流的速度就更快了,当然这也意味着不能混用它们了。
不过就算关闭了同步,C++/C 流仍然共用同一个文件描述符最终完成输出。
悲剧复盘
接下来我们来复盘悲剧是如何发生的:
- 通过
freopen()重定向到文件; - 关闭同步流,于是写入
Hello,World到 C++ 流的缓冲区; - 通过
fclose()刷新 C 流的缓冲区,并关闭文件描述符; - 程序结束,C++ 流的缓冲区刷新,但由于文件描述符已经关闭,它的输出失败,缓冲区里的东西也就被直接丢弃了。
这就是为什么没有输出。如果没有关闭文件描述符这一步,C++ 流就能成功刷新并输出。
至于没有关闭同步流的情况,这是由于在同步流开启的情况下,C++/C 流共享一个缓冲区,在fclose()这一步后就成功输出了。要是关闭了同步流,C++ 流用上了独立的缓冲区,fclose()就管不到它了,并且由于fclose()关闭了文件描述符,之后的刷新就无能为力了。
成事不足,败事有余。
解决方案
很简单,不要手滑写
fclose()。当然,如果你改不掉这个习惯,你应该在fclose()之前进行手动刷新操作。cout.flush(); cout<<fulsh; cout<<endl;这些都是可以的,它们会刷新 C++ 流的缓冲区。不过最好还是不要写
fclose()。后记
现在你终于知道了为什么会爆零!你斗志昂扬,决定下次一雪前耻。
不过还是先去买辣条吧。