Table of contents
Open Table of contents
TL;DR
ACM 模式写 C++ 的 I/O 模板只有两行:ios_base::sync_with_stdio(false); cin.tie(nullptr);——关掉 C stdio 同步和 cin 对 cout 的 tie,把默认慢的 cin/cout 提到接近 scanf/printf 的速度;然后用 cin >> 读 token、getline 读整行、stringstream 切分变长字段 这三招覆盖 95% 的输入格式;剩下 5% 的极限数据量(> 10^7 整数)用 fread + 手写 parseInt 解决。最容易踩的坑是 cin >> 和 getline 混用时换行符残留、scanf 读 double 用错 %f、输出循环里写 endl 拖慢几十倍。
为什么 ACM 对 I/O 特别敏感
工程开发里 I/O 性能通常不是瓶颈,但 ACM 不同:
- 数据量大 — 10^6 ~ 10^7 个整数是常态。默认的
cin/cout读 10^6 个 int 可能要 500ms+,直接把 1s 时限的题 TLE 掉 - 输入格式多样 — 定长矩阵、变长列表、带空格的字符串、多组测试数据、读到 EOF 停止,每种都有对应的最佳方案
- 时限严格 — I/O 吃掉 50% 时间的情况很常见,算法再好也跑不完
核心矛盾:I/O 慢到可以让同一份算法在 AC 和 TLE 之间来回切换。所以必须把 I/O 当成和算法同等重要的工程问题来处理。
第一层:默认的 cin/cout 为什么慢
两个独立原因:
1. 与 C stdio 同步(sync_with_stdio)
C++ 标准要求 std::cin/cout 默认与 C 的 scanf/printf 共享同一套缓冲区,这样两种 I/O 混用时顺序正确。代价:cin/cout 每次操作都要额外协调 C stdio 的缓冲区,实测比纯 scanf/printf 慢 3~10 倍。
2. cin 与 cout 绑定(tie)
默认 std::cin 被 tie 到 std::cout,含义是:每次从 cin 读输入前,先 flush cout 的缓冲区。这是为了让交互程序正确显示提示:
cout << "Enter name: "; // 未 flush,还在缓冲区
cin >> name; // 读之前自动 flush cout,用户才看得到提示
对 ACM 批处理场景这个 flush 完全没用,纯开销。
加速三连:写在 main 第一行
#include <iostream>
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
// 正文
return 0;
}
再加上一条:非交互题永远用 "\n" 代替 std::endl。endl 除了输出换行还会 flush 整个输出缓冲区,在输出 10^6 行时能把耗时从几十毫秒拖到几秒。
做完这三步,cin/cout 速度和 scanf/printf 差距在 20% 以内,绝大多数题够用。
代价(必须知道):
- 关闭 sync 后,不能再混用
cin/cout和scanf/printf,否则输出顺序乱掉 - 关闭 tie 后,程序必须先输出完再读输入的”交互”语义失效。非交互题无所谓
- 交互题必须不关 tie 或手动 flush,见下文专节
输入模式大全:覆盖 95% 情况的三把刀
模式 1:读定长 token(整数 / 浮点数 / 单词)
int n;
cin >> n;
double x, y;
cin >> x >> y;
// 等价 scanf
scanf("%d", &n);
scanf("%lf %lf", &x, &y); // double 必须用 %lf
cin >> 会跳过前导空白(空格、tab、换行),读到下一个空白停下。
模式 2:读定长数组 / 矩阵
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
int r, c;
cin >> r >> c;
vector<vector<int>> g(r, vector<int>(c));
for (int i = 0; i < r; ++i)
for (int j = 0; j < c; ++j)
cin >> g[i][j];
cin >> 对空白不敏感,不管输入是一行放完还是分多行放,都读得对。
模式 3:读含空格的整行字符串
string s;
getline(cin, s); // 读到换行为止,不包含换行符
不能用 cin >> s —— 它遇到空格就停,读不完整行。
模式 4:读变长一行(不知道这一行有几个数)
ACM 最常被卡的场景,比如输入”每行一个列表,长度不定”:
3 1 4 1 5 9
2 6 5
8 9 7 9
标准解法:getline 取整行,再用 stringstream 按空白切分:
#include <sstream>
string line;
getline(cin, line);
stringstream ss(line);
int x;
vector<int> row;
while (ss >> x) row.push_back(x);
stringstream 的 >> 和 cin >> 语义一致,按空白分隔读 token,读不到时 ss >> x 为假、循环退出。这是 getline + stringstream 的唯一通用组合,没有它就没有变长行输入。
模式 5:读到 EOF 停止(多组测试数据,不给组数)
经典模板题”A + B”就这样:
int a, b;
while (cin >> a >> b) {
cout << a + b << "\n";
}
// 等价 scanf:检查返回值(成功匹配的字段数)
while (scanf("%d %d", &a, &b) == 2) { ... }
cin >> x 返回 cin 的引用,在 while 条件中隐式转为 bool:EOF 或类型不匹配时为 false。
模式 6:读固定组数 T 的测试数据
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
// 每组独立处理
}
模式 7:按行读到 EOF
string line;
while (getline(cin, line)) {
// 处理一行
}
getline 同样返回流引用,可用于 while 判 EOF。
致命坑:cin >> 和 getline 混用
这是 ACM 和课程题最常被坑的问题,没有之一:
int n;
cin >> n; // 读完 n,换行符 \n 还留在缓冲区!
string line;
getline(cin, line); // 立刻读到一个空行(就是那个残留的 \n)
根因:cin >> n 按格式读,遇到空白(包括 \n)停下但不消耗它。下一个 getline 从 \n 开始读,立刻遇到换行返回空串。
正确做法:cin >> n 之后显式吃掉到行尾的所有字符:
#include <limits>
int n;
cin >> n;
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 吃到下一个 \n(含)为止
string line;
getline(cin, line);
简化但不严格的写法:cin.ignore(); 只吃掉一个字符——只要确认下一个字符一定是 \n 就够用。严格写法能处理 n 后面可能还有空格的情况。
反向场景同样要小心:getline 后接 cin >> 没问题,因为 getline 已经把 \n 消费掉了。
scanf / printf 速查表
需要极致速度或输入格式复杂(带特定分隔符)时,直接用 C I/O。
| 类型 | scanf | printf |
|---|---|---|
int | %d | %d |
long long | %lld | %lld |
unsigned int | %u | %u |
unsigned long long | %llu | %llu |
float | %f | %f |
double | %lf | %f 或 %lf |
long double | %Lf | %Lf |
char | %c | %c |
char* | %s(必须限长 %99s) | %s |
| 16 进制 int | %x | %x |
置信度说明:%lld 在 GCC/Clang/MSVC 现代版本都支持;老 MSVC 的 %I64d 已经过时(需验证——大约在 VS2013+ 后 %lld 就通用了)。
格式化输出示例:
printf("%.6f\n", x); // 保留 6 位小数
printf("%d %d\n", a, b);
printf("%5d\n", 42); // 右对齐宽度 5:" 42"
printf("%-5d|\n", 42); // 左对齐宽度 5:"42 |"
printf("%05d\n", 42); // 前补零宽度 5:"00042"
特定分隔符输入(scanf 独有的强项):
// 读 "2026-04-11"
int y, m, d;
scanf("%d-%d-%d", &y, &m, &d);
// 读 "12:30:45"
int h, mi, s;
scanf("%d:%d:%d", &h, &mi, &s);
scanf 格式字符串里非 % 字符必须字面匹配输入。cin >> 做不到这一点,遇到这种格式只能 getline 后手动解析。
关键限制:scanf("%s", ...) 读 C 字符数组,不能直接读 std::string:
string s;
scanf("%s", &s); // ❌ 未定义行为
char buf[1000];
scanf("%999s", buf); // ✅ 限长防溢出
s = buf;
cout 格式化输出:iomanip
#include <iomanip>
cout << fixed << setprecision(6) << 3.14159265 << "\n"; // 3.141593
cout << setw(5) << 42 << "\n"; // " 42"
cout << setw(5) << left << 42 << "|\n"; // "42 |"
cout << setfill('0') << setw(5) << 42 << "\n"; // "00042"
cout << hex << 255 << "\n"; // ff
cout << oct << 8 << "\n"; // 10
cout << dec; // 切回 10 进制
关键区别:fixed / setprecision / hex 等是粘性状态,一次设置影响后续所有输出,不像 printf 每次独立。setw 和 setfill 是一次性的,每次输出前都要重新设置。
极限性能:fread 手写快读
当数据量达到 10^7 个整数、连 scanf 都 TLE 时,下探到 fread 层:
#include <cstdio>
#include <cctype>
namespace fastio {
constexpr int BUF_SIZE = 1 << 20; // 1 MB
char buf[BUF_SIZE];
int buf_pos = 0, buf_len = 0;
inline char gc() {
if (buf_pos == buf_len) {
buf_len = (int)fread(buf, 1, BUF_SIZE, stdin);
buf_pos = 0;
}
return buf_pos == buf_len ? EOF : buf[buf_pos++];
}
inline int readInt() {
int x = 0, sign = 1;
char c = gc();
while (!isdigit(c) && c != '-' && c != EOF) c = gc();
if (c == '-') { sign = -1; c = gc(); }
while (isdigit(c)) { x = x * 10 + (c - '0'); c = gc(); }
return x * sign;
}
}
int main() {
int n = fastio::readInt();
// ...
}
为什么快:绕过 stdio 的 locale / 格式字符串解析开销,一次 fread 读一整块到用户缓冲区,再用最简单的状态机扫过。经验值比 scanf 快 2~5 倍(需验证——具体倍数因编译器和数据形态而异)。
什么时候用:
- n ≥ 10^6 且瓶颈确实在输入
- 输入只有整数/字符,格式简单
sync_with_stdio(false) + cin或scanf实测过不了
不要无脑套。90% 的题 sync_with_stdio(false) + cin 够了,手写快读的维护成本不小。
交互题的特殊处理
交互题(你输出一次,评测机读后回给你下一个查询)的核心要求是每次输出后必须 flush,否则输出留在缓冲区里,评测机收不到就死锁——本地看起来一切正常,提交上去直接 TLE。
// 交互题配置
ios_base::sync_with_stdio(false);
// cin.tie(nullptr); // ❌ 交互题禁用,或者就让它默认 tie 到 cout
// 每次输出后显式 flush
cout << query << endl; // endl 自带 flush,交互题反而要用
// 或
cout << query << "\n" << flush;
// 或
cout << query << "\n";
cout.flush();
口诀:非交互题用 "\n" 避免 flush;交互题用 endl 强制 flush。方向相反。
实战模板
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<int> a(n);
for (auto& x : a) cin >> x;
// 解题逻辑 ...
cout << "result" << "\n";
}
return 0;
}
<bits/stdc++.h> 是 GCC 扩展(一次性 include 所有标准库),非标准但 Codeforces / LeetCode / AtCoder 等平台都支持。生产代码别用——编译慢,且 MSVC 不支持。
本地调试小技巧:用 freopen 重定向 stdin/stdout 到文件,跑真实数据:
#ifdef LOCAL
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
编译时加 -DLOCAL 启用,提交时自动关闭。
Pitfalls 汇总
1. 关闭 sync 后混用 cin/cout 和 scanf/printf
ios_base::sync_with_stdio(false);
cin >> n;
printf("%d\n", n); // 输出顺序可能乱
为什么是坑:关闭 sync 后,C 和 C++ 的缓冲区独立,flush 时机不同,交错写的内容在终端/文件里顺序可能不是你期望的。 怎么避免:要么只用 cin/cout,要么不关 sync。不要跨阵营。
2. 循环里用 endl 代替 “\n”
for (int i = 0; i < 1000000; ++i) cout << i << endl; // ❌ 每行 flush
for (int i = 0; i < 1000000; ++i) cout << i << "\n"; // ✅
为什么是坑:10^6 行输出下,endl 能把输出时间从几十 ms 拖到几秒,轻松 TLE。
怎么避免:非交互题永远用 "\n"。endl 只在必须 flush 的场景(交互题、调试输出、程序结束前)才用。
3. scanf 读 double 用了 %f
double x;
scanf("%f", &x); // ❌ 读出来是垃圾值
scanf("%lf", &x); // ✅
为什么是坑:scanf 的 %f 告诉它按 float(4 字节)写入,而 double 是 8 字节——内存布局错位,读出来完全不是那个数。特别恶心的是 C++ 编译器通常不警告,只有加 -Wformat 才提示。和 printf 的规则(%f 接受 double)不对称。
怎么避免:记硬规则——scanf 读 double 用 %lf,printf 输出 double 用 %f 或 %lf 都行。
4. cin >> 之后 getline 读到空行
见上面专节”致命坑”。根因是 cin >> 不消耗 \n。
怎么避免:混用前一律 cin.ignore(numeric_limits<streamsize>::max(), '\n');。
5. scanf(“%s”, …) 尝试读 std::string
string s;
scanf("%s", &s); // ❌ UB
为什么是坑:%s 只认 char*,std::string 对象不是 C 字符串,写进去会破坏 string 的内部结构。
怎么避免:用 cin >> s,或先读到 char[] 再赋值。
6. 读字符串没设最大长度
char buf[100];
scanf("%s", buf); // ❌ 缓冲区溢出风险
scanf("%99s", buf); // ✅ 最多读 99 字符 + '\0'
为什么是坑:和 C 里的 gets 同类问题——输入比缓冲区长时写越界,非法内存写入。ACM 题虽然输入可控,但养成限长习惯避免意外。
怎么避免:永远写 %99s 形式的限长修饰。
7. 变长行用 cin >> 而不是 getline + stringstream
// 输入:第一行是个数 n,第二行是 n 个数
int n;
cin >> n;
vector<int> a(n);
for (auto& x : a) cin >> x; // ✅ 这种能用
但如果输入是”每行一个不定长列表”,cin >> x 不知道哪里是行尾,会贪心读到下一行。
怎么避免:凡是”一行代表一个逻辑单元、一行内字段数不定”的输入,必须 getline + stringstream。
8. 关了 sync 还在调 fflush(stdout)
ios_base::sync_with_stdio(false);
cout << "hello";
fflush(stdout); // ❌ 对 cout 的缓冲区无效
为什么是坑:fflush 操作的是 C stdio 的缓冲区,关闭 sync 后 cout 有自己独立的缓冲区,fflush 碰不到它。
怎么避免:用 cout.flush() 或 cout << flush / cout << endl。
9. cin >> 读字符数组时没判大小
char s[100];
cin >> s; // ❌ 超长会溢出
cin >> setw(100) >> s; // ✅ 限长,和 scanf("%99s") 等价
为什么是坑:同 pitfall 6,C 风格缓冲区溢出。cin >> 默认无限长读。
怎么避免:用 std::string 代替 char[],string 会自动扩容。
10. 忘记 cout 浮点精度默认只有 6 位
double x = 1.0 / 3;
cout << x << "\n"; // 0.333333(仅 6 位)
cout << fixed << setprecision(15) << x << "\n"; // 0.333333333333333
为什么是坑:竞赛题经常要 10^-9 精度,忘记设 setprecision 直接 WA。默认 6 位是 C++ 标准规定的,不是实现 bug。
怎么避免:主函数开头就设好 cout << fixed << setprecision(k),k 至少 10。
选型 Checklist
| 场景 | 首选 | 备注 |
|---|---|---|
| 一般算法题(n ≤ 10^6) | cin/cout + 关闭 sync | 最省心 |
| 数据量 10^6 ~ 10^7 整数 | scanf/printf | cin 也能试,scanf 更稳 |
| 数据量 > 10^7 | fread 快读 | 维护成本更高,按需使用 |
| 交互题 | cout << endl(不关 tie) | flush 是生命线 |
| 变长行输入 | getline + stringstream | 唯一通用方案 |
| 带特定分隔符(日期、时间、CSV) | scanf 格式字符串 | cin 处理不了 |
| 输出浮点固定精度 | cout << fixed << setprecision(k) 或 printf("%.kf") | 别忘设置 |
| 读含空格的字符串 | getline(cin, s) | cin >> 遇空格就停 |
| 混用 C 和 C++ I/O | 不关 sync | 否则输出顺序乱 |
延伸方向
- 竞赛 C++ 常用技巧 —
<bits/stdc++.h>、#define int long long(配合int32_t main())、typedef pair<int,int> pii等别名 istringstream/ostringstream—stringstream的读写专用版本,可以做字符串和数值类型的双向转换- locale 和 I/O —
imbue设置 locale 对性能的影响,一般 ACM 里不用管 - C++20
std::format— 更安全的格式化,但竞赛平台支持度需验证 - 调试 I/O —
cerr不参与 sync 开关,调试输出优先用cerr
信息来源和置信度
- 确定:
sync_with_stdio(false)+cin.tie(nullptr)的作用和副作用(C++ 标准 [iostreams] 章节 / cppreference) - 确定:
scanf中%lf读 double、%f读 float 的区别(C99 §7.19.6.2 / C++ 继承自 C) - 确定:
getline(cin, s)返回流引用、配合while判 EOF - 确定:
cin >> x不消耗遇到的空白字符(流格式化输入的标准行为) - 需验证:MSVC 对
%lld的支持年代(文中说 VS2013+,具体版本可能有偏差) - 需验证:
<bits/stdc++.h>在各平台的支持情况(GCC/Clang + libstdc++ 确定支持,MSVC / libc++ 不支持) - 需验证:
fread快读相对scanf的具体倍数,2~5 倍是常见经验值