C/C++ 输入输出详解:OJ 场景与性能优化
前言
在编程世界中,输入输出(I/O)是交互的基础。C 和 C++ 提供了强大的 I/O 机制,从简单的 printf 和 scanf 到 C++ 的流式操作 cin 和 cout,看似简单,实则暗藏玄机。许多开发者在使用过程中可能会遇到缓冲区问题、格式控制陷阱、性能瓶颈,甚至安全性隐患。本文将针对 C/C++ 输入输出的常见问题进行一一梳理。
1. OJ 题目输入情况汇总
在竞赛的 OJ 题目中,输入场景通常总结为以下几类:
1.1 单组测试用例
此类题目只需读取一次数据并输出结果。
示例:计算 (a+b)/c 的值 参考代码:
#include <iostream>
using namespace std;
int main() {
int a, b, c;
cin >> a >> b >> c;
cout << (a + b) / c << endl;
return 0;
}
示例:与 7 无关的数 思路:先输入 n,循环产生 1~n 的数字,找出与 7 无关的正整数,最后求平方和。与 7 相关的条件包括:被 7 整除、个位是 7、十位是 7。 参考代码:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int i = 1;
int sum = 0;
while (i <= n) {
if (i % 7 != 0 && i % 10 != 7 && i / 10 != 7) {
sum += (i * i);
}
i++;
}
cout << sum << endl;
return 0;
}
1.2 多组测试用例
1.2.1 测试数据组数已知
当题目明确告知有 n 组数据时,通常使用 while(n--) 循环。
示例:多组输入 a+b II 参考代码:
#include <iostream>
using namespace std;
int main() {
int n; // 表示数据组数
cin >> n;
int a, b; // 每行输入的 a,b 值
while (n--) {
cin >> a >> b;
cout << a + b << endl;
}
return 0;
}
示例:斐波那契数列 特点:前两个数的和是第三个数。有多种解法,如循环嵌套、预处理数组或递归。 参考代码(预处理数组):
#include <iostream>
using namespace std;
int main() {
int n, a, i;
int ret[35] = {0, 1, 1};
for (i = 3; i < 30; i++) {
ret[i] = ret[i - 1] + ret[i - 2];
}
cin >> n;
while (n--) {
cin >> a;
cout << ret[a] << endl;
}
return 0;
}
1.2.2 测试数据组未知
当不知道有多少组数据时,通常利用 while(cin >> ...) 来判断输入是否结束。
示例:多组输入 a+b 参考代码:
#include <iostream>
using namespace std;
int main() {
int a, b;
while (cin >> a >> b) {
cout << a + b << endl;
}
return 0;
}
这里 cin >> a >> b 返回流对象引用,在布尔上下文中检查流状态。如果读取成功则为 true,否则为 false。
1.2.3 特殊值结束测试数据
某些题目以特定数值(如 0 0)作为结束标志。
示例:多组数据 a+b III 参考代码:
#include <iostream>
using namespace std;
int main() {
int a = 0, b = 0;
while (cin >> a >> b, a && b) {
cout << a + b << endl;
}
return 0;
}
这里使用了逗号表达式,从左往右计算,结果为最后一个表达式的值。
2. 输入时特殊技巧
2.1 含空格字符串的特殊处理方式
读取含空格的字符串可以使用 fgets、scanf、getchar 或 getline。但有时将空格隔开的单词单独处理更方便。
练习:统计数字字符个数 做法一:读取整个带空格的字符串分析。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
getline(cin, s);
int ret = 0;
for (auto ch : s) {
if (ch >= '0' && ch <= '9') {
ret++;
}
}
cout << ret << endl;
return 0;
}
做法二:按照多个单词分析。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
int cnt = 0;
while (cin >> s) {
for (auto c : s) {
if (c >= '0' && c <= '9') cnt++;
}
}
cout << cnt << endl;
return 0;
}
判断数字字符也可以使用 isdigit(ch) 函数简化。
2.2 数字的特殊处理方式
输入 123 时,程序会根据变量类型将其解析为整型或字符串。根据实际需求选择处理方式。
练习:小乐乐改数字
法一:当做整数读取,通过 % 10 和 / 10 获取每一位。
#include <iostream>
#include <cmath>
using namespace std;
int main() {
int n;
int i = 0, ret = 0;
cin >> n;
while (n) {
if (n % 10 % 2 == 1) {
ret += pow(10, i);
}
n /= 10;
i++;
}
cout << ret << endl;
return 0;
}
法二:当做字符串处理,利用 ASCII 码奇偶性判断。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
cin >> s;
for (int i = 0; i < s.size(); i++) {
if (s[i] % 2) {
s[i] = '1';
} else {
s[i] = '0';
}
}
cout << stoi(s) << endl;
return 0;
}
stoi 是 C++11 标准引入的字符串转换函数。
3. scanf/printf 和 cin/cout 的对比
scanf 和 printf 是 C 语言标准函数,cin 和 cout 是 C++ 流对象。各有优缺点。
3.1 格式控制差异
scanf和printf需要手动指定格式字符串,容易出现格式错误,但格式化输出更精确直观。cin和cout自动识别类型,避免格式化错误,更易用。- 浮点数输出时,
cout默认忽略多余 0,printf默认打印 6 位小数。
#include <cstdio>
#include <iostream>
using namespace std;
int main() {
float a = 3.50;
double d = 16.50;
cout << "cout: " << a << " " << d << endl;
printf("printf: %f %lf\n", a, d);
return 0;
}
3.2 性能差异
结论:scanf 和 printf 通常比 cin 和 cout 快。
原因:cin 和 cout 为了兼容 C 语言,封装更复杂。且默认情况下,cin 与 cout 存在绑定关系,每次读取都会刷新输出缓冲区。在大数据量输入时,cin/cout 容易导致 Time Limit Exceeded。
案例演示
在大量数据输入场景下,使用 cin 可能超时,而 scanf 能通过。
3.2.2 优化方案和演示
可以通过以下代码优化 cin/cout 性能:
ios::sync_with_stdio(false); // 取消 C 语言输入输出缓冲区的同步
cin.tie(0); // 取消 cin 和 cout 的绑定
优化后,cin/cout 的性能可接近 scanf/printf。
总结
- 数据量较小(10^6 以内),两者皆可。
- 数据量较大(10^9 左右),推荐使用
scanf/printf或优化后的cin/cout。 - 极端大规模 IO 可使用快速读写模板。
结语
掌握输入输出的底层机制和陷阱,才能写出健壮高效的代码。从缓冲策略到格式控制,每个细节都可能成为调试时的关键。希望这篇文章能给各位对 C/C++ 的输入输出带来不一样的理解。


