资讯动态

算法竞赛C++输入输出上篇:scanf/printf与cin/cout避坑指南

发布时间:2026/10/21 19:56:58 来源:尧图企业网站定制
打算法竞赛的人十有八九都在输入输出上栽过跟头。题目的思路可能五分钟就想通了结果因为读入太慢卡了几百毫秒被 TLE或者代码逻辑完全正确却因为行尾多打了一个空格被判成 Presentation Error。C 的输入输出看着不起眼水却深得很。这篇博文先讲“上篇”把最常用的 scanf/printf、cin/cout、字符串读取、多组数据套路这些基本功掰开揉碎讲清楚。适合刚接触 OJ、准备参加算法竞赛的初学者也适合已经打了一段时间比赛、想系统梳理 I/O 细节的选手。看完至少能解决一大半“本地跑得好好的交上去就出错”的玄学问题。1. 先解决选择困难症scanf/printf 还是 cin/cout1.1 为什么 cin/cout 给人留下“慢”的印象很多人一进竞赛就开始用 cin/cout因为写起来确实舒服不用记格式符重载运算符对类型也友好。但很快他们就会发现同样一组大数据用 cin 读会超时换成 scanf 就稳过了。这里面的差距不是玄学而是 iostream 库默认要和 C 标准 I/O 保持同步。简单说为了保证 cin/cout 和 scanf/printf 混着用也不会乱序C 的流对象默认开启了一个同步机制每次 cin 读数据之前都要检查并同步缓冲区状态这是一个不小的开销。数据量大时这个开销就是致命的。类似“你开车每次起步前都要绕车一圈检查轮胎”这种感觉虽然安全但效率肯定上不去。那是不是竞赛里就一定不能用 cin/cout其实不是。C 提供了两个开关关掉之后速度能追平 scanf/printfios::sync_with_stdio(false); cin.tie(nullptr);第一句关闭和 C stdio 的同步第二句解除 cin 与 cout 的绑定。cout 默认和 cin 绑定意味着每次执行 cin 操作前都会 flush 一次输出缓冲区这种机制在交互式程序里有意义但在 OJ 判题环境中完全多余。实际效果我测过在某个百万级数据的读入题里加上这两句后 cin 读入耗时从 200 多毫秒降到 50 毫秒上下已经和 scanf 一个水平了。所以现在很多人的习惯是“全局写上这两句然后无脑用 cin/cout”。1.2 关同步之后的几个副作用必须知道关掉同步不是没有代价的。最大的副作用是不能混用 cin/cout 和 scanf/printf。原因是两边缓冲区不再同步你先用 cin 读入再用 scanf 读入可能出现 scanf 读不到数据或者读到脏数据的情况。所以一旦关了同步就老老实实全套用 iostream别想着哪个方便用哪个。我见过不少选手在同一个程序里写着ios::sync_with_stdio(false);然后又混用了scanf和getchar查了半天查不出问题。这里给个铁律关同步的代码里输入输出通道只能走 iostream 这一条路不关同步的时候混用也不推荐因为顺序容易乱。还有一个细节cin.tie(nullptr)解绑之后如果你用 cin 读入并用 cout 输出而且代码里指望“cout 先输出然后 cin 再等待输入”在某些平台上可能会出问题。不过普通 OJ 的判题都是输入输出分离的程序运行不自带交互基本不受影响。只有在写交互题或者自己用终端测试时才会踩中这个坑测试时注意一点就行。1.3 什么时候必须放弃 cin/cout尽管关同步后 cin 不慢我还是建议某些场景直接用 scanf/printf。第一个是读入量极大的情况比如几十万甚至上百万个数字哪怕关同步cin 的模板处理开销也比 scanf 的格式解析略多。第二个是输出大量内容时printf 的格式化输出在部分环境下比 cout 快一些特别是需要按固定宽度、补零、控制小数位数时printf 一个格式串就搞定cout 要各种 manipulator 来回拼。另外一个现实问题是老旧的 OJ 编译器。有些题目平台用的编译器版本比较旧对 iostream 优化的支持不完善关同步后速度提升有限。但 scanf/printf 无论哪个版本都稳定快。所以我的个人习惯是数据量小、写起来舒服用 cin/cout数据量大、追求极致稳定用 scanf/printf。两种都要熟练考试时才能灵活切换。2. 从 scanf 到 printf格式符和类型对应关系必须背牢2.1 常用格式符速查表scanf/printf 的灵魂就是格式符格式符写错轻则读入错误重则内存越界。我把竞赛里最常用的列成一张表类型变量声明scanf 格式符printf 格式符备注整数int a%d%d最常见长整数long long a%lld%lld注意不是 %d无符号unsigned int a%u%u较少用浮点数double a%lf%fscanf 必须用 %lf字符char ch%c%c不自动跳过空白字符串char s[100]%s%s遇空格或换行停止长双精度long double a%Lf%Lf输出也是 %Lf第一行和第二行最常见的错误就是把 long long 当成 int 读。比如变量声明成了long long n;写scanf(%d, n);看起来能过编译运行也不一定崩但读进来的数据已经被截断了。更危险的是printf(%d, n)这种写法long long 的高 32 位被丢掉了答案错了你甚至不知道错在哪。浮点数的坑更经典double 类型的变量scanf 必须写%lf而 printf 又用%f。很多新手把两者记反了在 scanf 里写%f或者在 printf 里写%lf结果是读入结果永远是 0 或者输出乱码。为什么 printf 可以用%f因为 float 在可变参数传递时自动提升为 doubleprintf 的%f实际处理的就是 double而 scanf 的%f只针对 float 指针所以 double 必须显式写成%lf。2.2 两个容易翻车的格式符细节一个是%c不跳过空白字符。假设你读取一个整数 n然后紧接着要读一个字符 ch你写int n; char ch; scanf(%d, n); scanf(%c, ch);输入5 A之后第二个 scanf 读到的其实是输入缓冲区里残留的回车符\n而不是A。解决办法是在%c前面加一个空格scanf( %c, ch);让 scanf 先跳过任意空白符。这个坑在写模拟题和处理括号、运算符表达式类题目时极其常见。另一个是%s遇空格就停。用%s读一行含空格的字符串是读不完整的。比如输入hello worldscanf(%s, s)只会读到helloworld还留在缓冲区里。需要读整行的场景要么用下面章节说的 getline要么用 C 语言里的扫描集scanf(%[^\n], s)这种写法来读但后者对字符串数组的长度控制要特别小心否则容易越界。字符串数组越界也是一个大坑。%s会在末尾自动补一个\0字符所以数组长度必须比最大字符串长度至少多 1。比如字符串最长 100 个字符读入数组至少要开 101。竞赛题里常常出现“字符串长度不超过 100”的描述然后有人开char s[100]去读最后程序在运行时报错或干脆 WA。这种问题本地测少数数据时不容易复现数据量一大就原形毕露。2.3 宽度、补零和小数位数的输出控制输出格式化在竞赛里同样有大量需求。最常见的是输出时间类题目比如要求输出HH:MM:SS秒数小于 10 时前面要补零。printf 可以写printf(%02d:%02d:%02d\n, h, m, s);02表示宽度为 2不足用 0 填充。%5d则是用空格填充宽度为 5。还有一类保留小数位数的题比如输出%.2f表示四舍五入保留两位小数。这里要注意printf 的四舍五入遵循 C 标准某些边界值如 0.005 这类二进制无法精确表示的小数可能出现精度问题竞赛中一般能接受但如果对精度要求极高建议在输出前给数值加上一个极小的 epsilon或者用整数运算避免浮点误差。用 cout 做同样的事稍微繁琐一点需要引入iomanip头文件cout fixed setprecision(2) x endl;fixed表示使用定点表示法记录小数位数没有这个的话setprecision(2)控制的是有效数字位数结果可能完全不一样。另外setw 和 setfill 配合可以完成 printf 中%05d的补零操作但要记得每次输出完 setw 就会失效不像 printf 的格式串那样持久。3. 字符和字符串的读取暗坑全在这3.1 单字符读取怎么用才不出错先说一下%c的另一个使用场景。很多时候我们需要逐字符读入一个矩阵、地图或者处理表达式的每个符号。scanf( %c, ch)的写法每次都会跳过换行和空格适合忽略空白符的连续字符读取。但如果地图本身包含空格字符比如原样输出带空格的地图那这种跳空白的方式就错了得直接读原始字符。还有一种方式是getchar()它每次读一个字符包括换行和空格不做任何过滤。getchar配合循环可以做到非常底层的输入控制后面讲快读的时候还会用到它的进化版。这里提示一句getchar()返回的是 int 而不是 char原因是为了能返回 EOF通常是 -1。如果你用 char 类型接返回值在判断c EOF时可能因为 char 有符号或无符号的问题导致死循环正确写法是int c; while ((c getchar()) ! EOF) { // 处理字符 }这个细节在 Windows 和 Linux 下表现还不一样Windows 下 char 默认有符号有些编译器下 char 接 EOF 还没事Linux 下很多环境 char 默认无符号c永远不可能等于 -1循环就永远跳不出去。3.2 读一行getline 家族的差异与残留换行字符串里最常用的读整行工具是getline(cin, string)它会把包括空格在内的一整行读进 string并且自动丢弃末尾的换行符。用法string line; getline(cin, line);但是这里藏着一个经典的神坑如果在这之前刚刚用cin n读过一个整数输入缓冲区里还残留着回车符紧接着调用getline它会立刻读到那个回车符返回一个空字符串。所以必须先清掉残留的换行cin n; cin.ignore(); // 丢弃一个字符通常是换行符 getline(cin, line);cin.ignore()默认丢弃一个字符在某些平台读取编译选项时如果用cin s和getline混用丢一个可能还是不够的甚至有的情况需要cin.ignore(numeric_limitsstreamsize::max(), \n)来跳过整行。不过竞赛里最常见的就是丢一个回车就够了。C 风格的gets已经被弃用了不建议再写。如果想读 C 风格字符串数组用fgets(s, sizeof(s), stdin)更安全但 fgets 会保留末尾的换行符处理时需要手动去掉比如s[strcspn(s, \n)] 0。或者用cin.getline(s, len)这个函数的语义是读到最多 len-1 个字符不会把换行符放入数组。那读多行不定数量的字符串怎么办比如每行是一个单词但不知道有多少行。可以用 while 配合 getline 一路读下去直到getline读到 EOF 时返回 false 为止。这种方式在处理文件型题目时很常用。3.3 一行里混着数字和特殊符号的解析竞赛题里经常出现一种格式一行里有若干整数数量不定比如1 2 3 4 5或者a1,b2这种带分隔符的输入。如果只是空格分隔的数字最简单的是int x; while (cin x) { // 处理 x }这会把文件里所有数字连续读出来不用关心换行。但如果一行内数字和字母混在一起比如输入格式是1234你要做的是解析出两个数。这时候有两个常见思路一是用 scanf 尝试读(%d),(%c),(%d)这样的格式串利用 scanf 的返回值判断是否成功二是把整行读进 string再用反向遍历或者其他方法手动处理。对于复杂格式我倾向于先把整行用getline读进来再结合stringstream切分stringstream ssin(line); int sum 0, x; while (ssin x) { sum x; }但 stringstream 在处理大数据量时效率偏低如果一行有上百万个数字建议还是直接用scanf循环读别用 stringstream 做过多的字符串拷贝。这个优化在数据解析类的题目里非常重要我有一次就是因为用 stringstream 处理一个几十万数据的序列白白多了几百毫秒的耗时。4. 竞赛里最常见的几类输入模式与对应写法4.1 单点测试与多点测试的区别算法竞赛的评测系统有两大类输入模式。一种叫单点测试程序只针对一组数据运行输出结果后程序结束另一种叫多点测试同一个程序会运行多次每次读入一组新数据。对应写代码的方式完全不同。单点测试通常就是最朴素的写法读入固定数量的变量算完就输出。多点测试则需要把整个处理过程包在 while 循环里否则程序读了一次就退出后面的数据根本没有被处理。很多新手第一次写某道题时本地测试单组数据没问题交上去全 WA原因就是没有写循环去反复读取数据。怎么判断题目是多点测试看题目的描述一般会说“输入包含多组测试用例每组第一行……”“处理到文件末尾”之类的话。如果没有这句话往往就是单点。还可以根据样例看样例里如果给出了两组输入示例那基本就是多组模式。4.2 T 组数据与以 0 结束的套路多点测试里最基础的模式是“第一行一个整数 T表示测试组数接下来是 T 组数据”。写法非常固定int T; cin T; while (T--) { // 每组数据处理 }注意T--的妙用先判断 T 非零再减一循环次数正好是 T 次。如果写成while(T)也行别把while(T--)和for(int i0; iT; i)混了就行。另一种常见模式是“以 0 输入作为结束标志”。比如输入 nn0 时程序结束。写法上可以是int n; while (cin n n ! 0) { // 处理非零 n }也可以写成while(scanf(%d, n) 1 n)。这里很多人写while(scanf(%d, n))这个写法有点危险因为 scanf 成功读入时返回的是成功读入的项数读入一个整数就是 1但读到文件结尾时返回 EOF负数在大多数环境下一共循环一次或两次就会退出。逻辑好像碰巧能过但不太严谨。标准写法还是判断 1或! EOF。4.3 EOF 判断怎么写最稳多点测试的另一种描述是“输入包含多组数据处理到文件结束”。这时候读取循环不能依赖 T 或 0 结尾要自己判断 EOF。C 风格写法int a, b; while (scanf(%d%d, a, b) ! EOF) { printf(%d\n, a b); }读入成功时 scanf 返回成功读入的项数读两个数就是 2返回值可能是 2也可能是 1比如只有最后一个数所以判断! EOF是最安全的。也可以写成 2表示必须成功读入两个数才处理这个在某些边界数据下更严格能避免读到不完整数据时误处理。用 iostream 就简单了while(cin a b)本身就在判断流是否正常EOF 时表达式为 false循环自动结束。这个写法和 scanf 版的作用完全等价看个人喜好。我个人在有大数据量需求时用 scanf 版在普通题里用 cin 版两种都要会写。还有一个偏门的判断写法while (~scanf(%d, n))~是按位取反EOF 如果定义成 -1取反后就变成 0循环结束。这个写法在部分老牌 OJ 上非常流行新手第一次看到可能懵但理解了原理就知道这只是“不等于 EOF”的另一种写法。知道即可不建议主动用。5. 输出格式的细节才是决胜点5.1 行尾空格与空行——PE 的根源输入正确、算法正确输出却因为格式被判错这是最憋屈的死法。OJ 对输出格式的判断通常有两种一种是 Special Judge只检查数字答案是否一致不关心空白另一种是严格比较把整行输出当字符串比对多一点空格都判 Presentation Error 或 Wrong Answer。最常见的格式要求是“输出样例之间以空行隔开”或者“每组输出结果占一行两组输出间空一行”。这个东西处理起来非常闹心尤其是你用一个 while 循环输出多个样例时很容易在最后一行后面多加一个换行符或者缺少样例之间的空行。经验做法是用一个变量记录当前是否已经是第一个输出块除第一块外每块输出前先打印换行或空行。比如bool first true; while (...) { if (!first) cout endl; // 两组之间空行 // 输出本组结果 first false; }这样就不会在最后一个结果后多输出空行。同样的思路也适用于“行尾不能有多余空格”的情况把当前行要输出的多个数通过一个 flag 控制第一个数前不打印空格之后的数前打印一个空格bool first true; for (int i 0; i n; i) { if (!first) cout ; cout arr[i]; first false; } cout endl;别看这些都是小细节我统计过自己打比赛罚时最多的原因除了算法不熟输出空白符错误绝对能排进前三。改代码两分钟重新提交五分钟一场比赛四个小时几分钟的 penalty 都可能在排名上产生巨大影响。5.2 调试输出的纪律调试的时候很多人习惯往代码里塞一堆 printf 看中间结果这无可厚非但提交之前必须清理干净。我有一次在某道模拟题里加了几行调试输出提交后 WA检查半天发现是调试输出干扰了正常输出格式删掉就好了。更糟糕的是某些题目的输出需要严格匹配多一行调试输出直接 PE。有一种更稳妥的调试输出方式是用 cerr 代替 cout。cerr 输出到标准错误流在 OJ 判题时会和标准输出分离开通常不影响答案比对。不过 cerr 没有配缓冲区频繁调用很慢大量循环里调试还是会拖慢程序所以只能在小数据量调试时用。如果题目要求输出固定数量的数用的是循环输出我建议提交前把临时变量的输出彻底注释掉再对拍几组数据确认输出格式。对拍的方法也很简单写一个随机数据生成器跑一遍你的程序和另一份确定正确的暴力程序用 diff 比对输出结果是否一致。这个过程可以自动化网上有很多现成的对拍脚本模板找个时间配置一次后面省心非常多。6. 常见问题速查与排查思路6.1 输入输出相关的经典头铁错误把我在当选手和后来带队伍时见过的各种灵异问题汇总一下做成一张速查表。遇到问题先对着表查一遍能省下大量瞎试的时间症状可能原因解决办法大数据时 TLEcin 没关同步加 ios::sync_with_stdio(false)读入的数是 0 或乱码scanf 格式符和类型不匹配比如 long long 用了 %d对照格式符表检查浮点数输出全是 0.00double 用了 %f 输出正确但 scanf 用了 %f 读入scanf 用 %lfprintf 用 %f字符变量读到换行或空格未在 %c 前加空格写 scanf( %c, ch)字符串读不完整用了 %s 读含空格的行改用 getline交上去莫名 WA调试输出没删干净仔细检查 printf/cout输出末尾多空行循环后多打了 endl用 first 标志控制空行本地对拍正确但 OJ 上 WA数组越界字符串长度不够开大一倍如 105输出中文出现乱码平台编码差异竞赛中一般只用英文输出读一行空字符串cin 后紧跟 getline加 cin.ignore()还有一个经常出现的错误是忘记取地址。scanf 需要变量地址不是每个新手都记得在变量前加尤其是读字符串时s本身就是地址不需要再加但如果读的是s[i]又要写s[i]。这类细节容易混。建议写完代码后养成一个习惯把每个 scanf 的参数都过一遍确认类型和地址是否匹配再提交。6.2 快读快写给“下篇”留个引子当你要处理上百万甚至千万级的数据哪怕 scanf 都不够快时就需要“快读”。快读的原理很简单直接用getchar()逐字符读入然后手动解析成整数完全绕过格式化解析的开销。很多竞赛老兵都有自己的模板比如inline int read() { int x 0, f 1; char c getchar(); while (c 0 || c 9) { if (c -) f -1; c getchar(); } while (c 0 c 9) { x x * 10 c - 0; c getchar(); } return x * f; }这段代码核心是一个字符一个字符地读跳过空格和换行通过字符和数字的 ASCII 码关系算出数值。负号单独用 f 标记处理。类似的输出端也可以写一个基于putchar的快写把一个整数倒序逐位输出。不过快读快写属于性能和细节的结合体里面还有不少坑比如 EOF 处理、无符号数、负数读入、以及用什么方式递归输出整数等等。这些内容放到“下篇”单独讲上篇先把基本盘练扎实。等你在普通题里已经形成肌肉记忆再去碰快读会轻松很多。我自己的体会是输入输出这块没有太多天赋成分纯粹是经验积累。你踩过的每一个坑下一次见到类似题都会自动绕开。建议花半天时间把上面这些例子都在本地跑一遍尤其是 getline 残留换行和 scanf 格式符不匹配这两个问题亲手踩一次比看十遍文章都管用。基础稳了后面学算法才不会被这些边角料绊住手脚。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑