判断EOF的四种写法与避坑指南)
做 OJ在线评测的朋友应该都有过这种经历题目逻辑半天就写完了结果输入处理不对交上去一个 WA心态直接崩了。尤其是“输入有多组数据读到 EOF 结束”这种描述新手往往一脸懵——什么叫读到结束我程序怎么知道什么时候该停这时候 while 循环配上 cin.get() 就是最常用的解法。我见过太多人卡在这套组合上要么循环根本不结束要么多读了一个换行要么本地跑得好好的上 OJ 就错。其实 OJ 的输入处理是有固定套路的今天就把 while cin.get() 判断结束这件事从头到尾讲透顺带把 cin、getline、getchar 这些容易搞混的东西也一起理清楚。1. 为什么 OJ 题目总跟“判断输入结束”过不去1.1 OJ 评测系统的输入输出逻辑先搞清楚 OJ 到底怎么测你的程序。OJ 全称 Online Judge在线评测系统。评测的大致流程是系统拿到你提交的源码编译运行然后喂给它一份预先准备好的输入数据通常是 .in 文件再拿你的程序往标准输出写的内容和标准答案做对比。这就带来一个和“本地 IDE 里跑程序”完全不同的关键差异在本地你习惯用键盘敲数据程序等你输入在 OJ 上程序不是和你交互的而是像一条水流管道——从 stdin 读数据往 stdout 写数据。你根本不知道输入文件里有多少数据也不知道什么时候算结束。所以题目里才会频繁出现这些描述“输入包含多组测试数据处理到文件末尾”“每组数据占一行以 EOF 结束”“输入包含多行请统计……直到输入结束”这种“不知道有多少个”的输入必须靠循环去读而循环什么时候停就得靠读取函数的返回值来判断是否已经到达 EOF。EOF 就是 End Of File文件结束标志。在 C/C 里它通常定义成 -1。1.2 常见的三种输入格式约定做了几十道 OJ 题之后你会发现输入格式翻来覆去就三种**第一种先给数量。**题目说“第一行是一个整数 n接下来 n 行数据”。这种最简单先 cin n再用 for 循环读 n 次就行。本质上“读几次”是已知的不需要判断结束。**第二种特定值结束。**题目说“输入多个整数以 0 表示输入结束”或“遇到 END 停止”。这是用一个哨兵值来代替 EOF你读到一个特殊值就 break 或让循环条件不满足。**第三种EOF 结束。**没有数量、没有哨兵输入文件有多少数据就处理多少。这时候就必须用 while 循环配合读取函数的返回值读到 EOF 就退出循环。而 cin.get() 恰恰就是一种能捕获 EOF 的读取函数。我自己的经验是拿到题先别急着写逻辑先把输入约定看清楚确认属于哪种类型再选对应的处理套路。很多 WAWrong Answer不是算法错是输入类型判断错了。1.3 while 循环为什么是处理输入的核心“读到不能再读为止”这件事天然就是循环。你可以想象成吃自助餐你不知道总共有多少盘菜只知道吃到餐盘见底就停。while 就是那个一直夹菜的动作cin.get() 就是“夹菜的手”它夹到空盘子EOF就告诉你没菜了。所以 OJ 输入处理的核心思维就一句话**循环条件负责判断“还能不能读到”循环体负责处理读到的内容。**while 是外壳cin.get() 是探针两者组合就是对“未知数量输入”最通用的解法。2. cin.get() 到底能做什么三个重载逐个讲清楚很多人对 cin.get() 敬而远之是因为它有三个重载版本记不住谁是谁。其实你把三个版本分开看一点都不复杂。2.1 cin.get() 无参版本读单个字符并返回 int这是最基础、也是判断 EOF 时最关键的版本。用法是int ch cin.get();它从输入流里读一个字符包括空格、换行、制表符这些空白字符然后返回这个字符的整数值。如果读到文件末尾或者发生错误它返回 EOF也就是 -1。这里有个新手最容易踩的坑为什么返回值要用 int 而不是 char因为 char 可能是有符号的也可能是无符号的在不同编译器里范围不一样。比如某个平台 char 是无符号的范围是 0~255那就永远装不下 -1就算是有符号的 char你拿它和 EOF 比较也容易因为类型提升搞出问题。所以标准写法是定义int ch而不是char ch。至于为什么需要 int 才能装下 EOF你可以把 cin.get() 想成一个“快递员”正常情况下他送来的包裹是 0~255 范围内的字符编号但遇到文件结束时他送来的是一张写着“没货了”的纸条这个纸条的编号是 -1。你如果用一个只能装 0~255 的箱子去接那张纸条就装不下信息就丢了。2.2 cin.get(char) 引用版本读字符存入变量这个版本长这样char ch; cin.get(ch);它也是读一个字符同样不跳过空白但把读到的字符存进你传入的变量里。它和无参版本最大的区别是返回值这个版本返回的是流对象本身类型是 istream所以它可以被转换成 bool 值——读取成功时条件为 true读到 EOF 或出错时条件为 false。因为这个特性它特别适合直接塞进 while 条件里while (cin.get(ch)) { // 处理 ch }这里不需要你手动去比较 EOF流自己知道自己什么时候读不到东西了。我后面会专门讲这个写法为什么比while ((ch cin.get()) ! EOF)更不容易写错。2.3 cin.get(char*, n) 数组版本按块或按行读取第三个版本是用来读一串字符的char buf[100]; cin.get(buf, 100);它最多读取 99 个字符n-1 个或者遇到换行符停止然后在末尾自动补上 \0。这句话里有两个关键点第一它遇到换行符就停但不会把换行符从输入流里取走。也就是说换行符还留在缓冲区里。如果你紧接着再调一次 cin.get(buf, 100)会因为第一个字符就是换行符而直接失败。所以用这个版本循环读行的时候需要在每次读完后再用cin.get()或cin.ignore()把换行符“吃掉”。第二如果它什么都没有读到比如输入流已经在 EOF 状态流会设置 failbit返回的流对象转换成 bool 就是 false。这就让我们可以用它作为循环条件。这个版本平时用得比前两个少因为 C 里按行读有更舒服的getline。但在一些老题、C 风格字符数组要求的代码里你还是会见到它。2.4 与 cin 、getline、getchar 的对比很多人搞不清楚cin 和cin.get()到底差在哪。一句话总结cin 会跳过空白字符遇到空白就停cin.get()不跳过空白什么都往里读。读取方式是否跳过空白是否处理换行能否判断 EOF典型用途cin ch跳过不读入停在空白处流对象判断读数字、单词、单个非空白字符cin.get()无参不跳过读入换行返回 EOF(-1)逐字符处理判断结束cin.get(ch)不跳过读入换行流对象转 bool逐字符处理循环cin.get(buf, n)不跳过不读入换行换行留在流中流对象转 bool按块/按行读 C 风格字符串getline(cin, str)不跳过读入并丢弃换行流对象转 bool按行读 stringgetchar()(C)不跳过读入换行返回 EOF(-1)C 语言逐字符处理这张表建议存一下每次拿不准就翻一翻。尤其注意cin.get(buf, n)和getline(cin, str)在处理换行上的差异这是很多隐蔽 bug 的来源。3. while cin.get() 判断结束的四种标准写法现在进入正题。基于上面的原理OJ 里判断输入结束有几种成熟写法我一个个说清楚顺便告诉你怎么选。3.1 写法一while ((ch cin.get()) ! EOF)#include iostream using namespace std; int main() { int ch; while ((ch cin.get()) ! EOF) { // 这里 ch 就是读到的字符可能是空格、换行也可能是普通字符 } return 0; }这是最经典、最直接的写法。原理就是利用无参版本的cin.get()返回 EOF(-1) 这个特性每次循环先读一个字符赋给ch然后拿它和 EOF 比较不相等说明读到了有效字符进入循环体相等说明文件结束退出循环。这里有两个必须注意的细节第一个ch一定要声明成int。前面已经说过用char会装不下 EOF在某些平台上会导致死循环或者误判。第二个括号不能丢。(ch cin.get()) ! EOF外面的括号是必须的因为赋值运算符的优先级低于关系运算符!。如果写成while (ch cin.get() ! EOF)实际会被解析成ch (cin.get() ! EOF)也就是把“是否不等于EOF”的结果赋给 ch语义完全错了。这个错误编译器不一定报错但跑起来行为会很诡异。这种写法最贴近 C 语言的while ((c getchar()) ! EOF)如果你熟悉 C用起来会非常顺手。在 C 里它也完全没问题。3.2 写法二while (cin.get(ch))#include iostream using namespace std; int main() { char ch; while (cin.get(ch)) { // 处理 ch } return 0; }这种写法的核心是“用流对象本身的状态来判断”。cin.get(ch)读成功时返回流对象这个对象在布尔语境下是真读失败到达 EOF时流内部会进入 fail 状态对象转成 bool 就是假循环自然结束。相比写法一这种写法的好处是不需要自己声明 int、不需要记 EOF 这个特殊值不容易写错优先级循环体里直接用 char 类型操作省去 int 转 char 的麻烦语义更“C”我们不关心是否遇到 EOF只关心“刚才那次读取到底成功没有”我个人的习惯是在 C 代码里优先用这种写法。它在逻辑上更不容易出错读代码的人一眼就能明白“只要还能从 cin 里取出一个字符就继续处理”。这里有个小细节要提醒如果用cin.get(ch)当读取失败时ch的值是未定义的标准里没规定它必须变成什么所以不要在循环退出后还去用ch的值做处理。正常逻辑也不会但排查死循环时要知道这点。3.3 写法三while (cin.get(buf, n)) 按块/按行读取#include iostream using namespace std; int main() { char buf[256]; while (cin.get(buf, 256)) { cout buf endl; cin.get(); // 关键把换行符从流里取走 } return 0; }这种写法适合按“块”读取的场景。cin.get(buf, 256)会尝试读最多 255 个字符或者读到换行符为止。如果读取成功返回的流对象为真进入循环如果已经到 EOF流进入失败状态循环结束。cin.get()那一行是必须的因为cin.get(buf, 256)不会把换行符从输入流中取走如果你不处理下一次循环会立刻因为第一个字符是换行符而失败然后循环直接退出。很多人的程序“只读了一行就莫名其妙结束了”多半就是这个原因。补充一句如果你只是要按行读字符串C 里更推荐用getline(cin, str)。cin.get(buf, n)更像是“必须在 C 风格字符数组里存结果”时的选择。3.4 C 语言版while ((c getchar()) ! EOF)虽然标题讲的是 C但 OJ 的输入处理思路是通用的。C 语言里对应写法是#include stdio.h int main() { int c; while ((c getchar()) ! EOF) { putchar(c); } return 0; }getchar()和cin.get()无参版本行为几乎一模一样读取一个字符包括空白返回 intEOF 时返回 -1。所以很多从 C 转 C 的人会把while ((c getchar()) ! EOF)的习惯带过来改成 C 就是写法一。要提醒的是如果你c声明成char在某些平台特别是 char 默认是 unsigned 的情况下c ! EOF的判断永远不会成立程序就会陷入死循环。这就是我一直强调用int的原因。遇到这种“本地好好的OJ 上超时/死循环”的诡异问题优先检查是不是 char 类型加上 EOF 判断惹的祸。3.5 四种写法怎么选直接给结论逐字符处理、追求极致底层控制或者你有 C 语言习惯用写法一C 项目、逐字符读取、想要代码更安全更简洁用写法二必须用 C 风格字符数组按行读用写法三写 C 语言题目用 3.4 的getchar()版本无论选哪个底层判断结束的原理都一样EOF 就是 -1流对象在读取失败时会变成“假”。理解了这一条四个写法随便切。4. 实操三个高频 OJ 题型的完整实现光讲原理不够我拿三个 OJ 上出现频率很高的题型把完整代码和测试方法都过一遍。4.1 题型一统计字符个数/种类读入直到 EOF题目描述示例输入一段文本统计其中大写字母的个数处理到文件结束。分析这是最典型的“按字符读取直到 EOF”的题。因为要数每个字符所以用cin.get()逐字符读最合适。完整代码#include iostream using namespace std; int main() { int ch; int cnt 0; while ((ch cin.get()) ! EOF) { if (ch A ch Z) { cnt; } } cout cnt endl; return 0; }如果你想用写法二代码长这样#include iostream using namespace std; int main() { char ch; int cnt 0; while (cin.get(ch)) { if (ch A ch Z) { cnt; } } cout cnt endl; return 0; }这两种上 OJ 都能正确跑。区别只在代码风格和编译后的细节行为结果完全一样。这里有一个小地方值得说**统计字符数量时要注意空白字符也被读进来了。**如果你要统计的是“字符总数”换行符和空格都会算进去如果题目要求“去掉空白后的字符数”你需要自己过滤if (ch || ch \n || ch \t) { continue; }很多 WA 就是栽在这种细节上。4.2 题型二按行处理直到文件结束题目描述示例输入多行字符串每行输出该字符串的长度。这种题按字符读也行但更自然的思路是“一次读一行、处理一行”。C 里最舒服的写法是getline#include iostream #include string using namespace std; int main() { string line; while (getline(cin, line)) { cout line.size() endl; } return 0; }getline(cin, line)每次会读一整行包括空格读到换行符停止并且把换行符从流中取走并丢弃。它返回流对象所以同样可以放进 while 条件里判断 EOF。如果题目指定要用 C 风格字符数组那就得用cin.get(buf, n)的写法注意吃掉换行#include iostream using namespace std; int main() { char buf[256]; while (cin.get(buf, 256)) { cout buf endl; cin.get(); // 吃掉行尾的换行符 } return 0; }这里有一个“如果最后一行没有换行符”的潜在坑某些测试数据的最后一行是文件末尾直接结束没有\n。此时用cin.get(buf, 256)迭代时最后一次读取可能成功但后面cin.get()直接遇到 EOF没问题因为循环已经处理完数据了。真正要注意的是不要画蛇添足地在循环后再加一次cin.get()去“确认吃掉换行”那反而会丢掉有效状态。还有个常见变体题目说“输入每行包含若干整数以空格分隔处理到 EOF”。这种可以按行读下来再解析也可以直接用cin x循环——因为cin x本身会跳过空格和换行遇到 EOF 时返回的流对象为假int x; while (cin x) { // 处理 x }注意cin x这种写法会丢掉“行”的概念。如果题目要求“每行输出一个结果”但输入里不同行的数据互相独立其实你按数据流处理也不影响结果。但如果你必须感知换行就得用getline读整行再手动拆分比如用stringstream。4.3 题型三以特定值结束的对比题目描述示例输入若干整数以 0 结束求所有非零整数的和。这类题不需要判断 EOF而是看“哨兵值”#include iostream using namespace std; int main() { int n; int sum 0; while (cin n n ! 0) { sum n; } cout sum endl; return 0; }这里循环条件有两个判断cin n成功没有到 EOF并且 n 不等于 0。从语文上翻译就是“只要还能读到一个数而且这个数不是 0就继续累加”。用的短路特性cin n如果失败比如文件读完、或遇到非整数内容后面的n ! 0根本不会执行不会去访问一个无效的 n安全。这种题和 EOF 判断的区别在于EOF 判断是“读到文件自然结束”哨兵值是“读到一个约定的停止标记”。如果题目说“输入以 0 结束但 0 后面可能还有内容”那还要考虑是否跳过 0 后面的内容不过绝大多数题目不会设计得这么绕。4.4 本地测试时如何模拟 EOF很多人在本地测试 EOF 循环时程序一直卡在终端等输入以为死循环了。其实不是是终端不知道你“输入完了”。在 Windows 的命令提示符或 PowerShell 里模拟 EOF 是按CtrlZ然后回车。注意一般要求 CtrlZ 出现在一行的开头之后再按回车程序才会收到 EOF。在 Linux 或 macOS 的终端里模拟 EOF 是按CtrlD。在一行的开头按 CtrlD终端就会关闭输入流程序里的cin.get()就会返回 EOF。更推荐的方式是用文件重定向测试把输入数据写到文件 input.txt然后终端执行./a.out input.txtWindows 下类似a.exe input.txt这样程序从文件读读到文件结尾自然就是 EOF你不用手动按任何组合键而且复现方便数据可以反复改、反复测。这是我在本地测 OJ 题最推荐的方式。你还可以用管道echo -e abc\nDEF\n | ./a.out这也能让程序从标准输入读数据然后自动遇到 EOF。总之别再用“手动敲键盘然后发呆”的方式测试 EOF 循环了。5. 常见问题与排查技巧实录最后我想把这些年实际踩过的坑集中列出来。这些问题我在带新人、看别人代码时反复遇到写成一个速查表给你。5.1 为什么循环不结束、一直等待输入最常见原因就是在本地终端手动测试时没有给 EOF。你以为程序卡死了其实它在老老实实等你继续输入。先按 CtrlDLinux/macOS或 CtrlZ 加回车Windows试试。其次是逻辑问题如果你用了while (cin.get(ch))理论上读到 EOF 必然结束不会死循环。会死循环的反而多见于用char ch配合while ((ch cin.get()) ! EOF)因为 char 装不下 -1。解决方法是把 ch 改成 int。还有一种情况题目要求“以 0 结束”你用了 EOF 判断那当然永远等不到 EOF因为测试数据最后没有文件结束标志的“约定”只有 0。所以写循环前一定先看清楚题目输入约定。5.2 为什么多读或少读字符Windows 下的 \r 问题如果你在 Windows 本地生成测试文件再重定向给程序读会发现按行读出来的字符串末尾可能带着一个\r回车符ASCII 13。这是因为 Windows 的文本文件换行是\r\nLinux 下是\n而 OJ 的测试数据通常在 Linux 上运行。遇到这种情况统计字符或者按行处理时可以显式忽略\rwhile (cin.get(ch)) { if (ch \r) continue; // 处理其余字符 }或者按行读时把行尾的\r去掉if (!line.empty() line.back() \r) { line.pop_back(); }好消息是OJ 的输入数据一般不会有这个问题因为评测机是标准 Linux 环境。但你在本地 Windows 上测试时有这个问题如果不对齐就会出现“本地测试正确OJ 上 WA”的反向情况。所以本地测试尽量生成 Linux 换行LF的文件或者代码里做兼容处理。5.3 while (cin.get(ch)) 和 while ((ch cin.get()) ! EOF) 到底有什么区别很多初学者以为这俩完全等价其实在细节上有差别。第一个是类型问题cin.get(ch)的 ch 是 char循环体里直接用 charcin.get()返回 int要在循环体里打印或比较字符时往往需要强转成 char。第二个是失败行为cin.get(ch)在读取失败时不会给 ch 设置任何有意义的“EOF 值”它只是让流状态变成 fail然后循环条件为假退出。你在循环外不应该依赖 ch 的值。而cin.get()版本通过返回 -1 来传递 EOF 信息你可以把 EOF 这个值拿出来做更多判断虽然实际很少需要。第三个是语义风格流对象版本更 CEOF 值版本更 C。两种都能在 OJ 上过没有说哪个绝对更好。但如果你还在学 C我建议你从while (cin.get(ch))开始因为它的条件更直观不容易踩类型坑。5.4 为什么程序死循环或者多处理了一次不要用 !cin.eof()我见过的最经典错误写法是while (!cin.eof()) { cin.get(ch); // 处理 ch }这个写法有问题eof()标志不是“提前”告诉你会不会到 EOF而是“已经读到越过文件末尾”之后才被设置。也就是说当你读入最后一个字符后cin.eof()还是 false循环体还会再进去一次那次cin.get(ch)读取失败但你已经把失败前的旧 ch 又处理了一遍导致结果多算了一次。正确做法永远是用读取函数的返回值判断不要先判断 eof 再读。用while (cin.get(ch))或while ((ch cin.get()) ! EOF)读取和判断是一体的不会出现“多处理一次”的问题。5.5 输入处理速查表场景推荐写法先给 n再读 n 组int n; cin n; for (int i 0; i n; i) {...}读到特定值停止while (cin x x ! stop)逐字符读到 EOFwhile (cin.get(ch))或while ((ch cin.get()) ! EOF)逐行读到 EOFstringwhile (getline(cin, str))按块读到 EOFchar 数组while (cin.get(buf, n)) { ...; cin.get(); }C 语言逐字符读到 EOFwhile ((c getchar()) ! EOF)读若干以空格分隔的数到 EOFwhile (cin x)再补一个我个人的习惯凡是输入里强调“多组测试数据”的我写代码第一行就是确认读取循环先把骨架搭好再往循环体里填业务逻辑。这样能避免写完整段逻辑后发现输入处理方式不对推倒重来。最后再分享一个小技巧如果你实在拿不准 OJ 的输入格式可以把样例输入复制到本地文件用./a.out sample.txt测一遍再对照样例输出。一旦这个过程通过再提交上去的把握就大了很多。这个习惯比记一百个 API 都有用。