本文是为了教一个小学弟写的,为了文章完整性我全面总结了一些 OI 中经常用到的哪些输入输出技巧以及注意事项
std::cin
与 std::cout
我相信你的第一行 C++ 代码一定是下面这个:
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << "Hello, world!" << endl;
return 0;
}
接下来你一定还学过 cout
用于输入。但是这里有两个小问题,第一是在 CCF 举办的 NOI 赛事中,使用 using namespace std
有概率导致你的程序在 Windows 平台正常运行但在测评环境出错;第二是 endl
会强制刷新缓冲区,导致输出效率变慢。所以我更推荐你使用下面的写法:
#include <bits/stdc++.h>
int main() {
std::cout << "Hello, world!" << '\n';
return 0;
}
顺带讲一下 using namespace std
的作用吧。这行代码表示“使用 std
命名空间”。如果你不理解,那么你只需要记住,使用这行代码可能会导致你的一些变量名与内置的一些符号产生冲突,经典的案例就是 next
和 y0
等。删除这一行代码之后,你无脑的在调用内置函数之前加上 std::
前缀即可。特别的,如果某个函数添加 std::
后出现问题但不添加就能正常运行,那你无脑不添加就好了,这往往是由于某些 C 语言的特性与 C++ 冲突而编译器不能很好地处理导致的。
std::getline
另外值得一提的是,你可以使用 std::getline
读取一整行输入:
std::string s;
std::getline(std::cin, s);
另外还有一个 getline
方法可用于输入一行内容到 char[]
:
char s[256];
std::cin.getline(s, 256);
printf
与 scanf
想必你一定也见过这两个函数。敏锐的你也许会注意到,这两个函数我并没有添加 std::
,这是由于这两个函数源自 C 语言,因此也被称为 C 风格输入输出;相应地,std::cin
与 std::cout
被称为 C++ 风格的。你也许经常听到有人说这 C 风格的输入输出比 C++ 风格的更快,在一般情况下确实如此。
顺带一提的是 C 风格输入输出的格式化字符串,这里给出一个列表用于快速查询:
整数类型
- 有符号整数
%d
或%i
: 十进制整数%ld
或%li
: 长整型(long int
)%lld
或%lli
: 长长整型(long long int
)
- 无符号整数
%u
: 无符号十进制整数(unsigned int
)%lu
: 无符号长整型(unsigned long int
)%llu
: 无符号长长整型(unsigned long long int
)
- 八进制
%o
: 八进制整数(默认为int
)%lo
: 无符号长整型的八进制表示(unsigned long int
)%llo
: 无符号长长整型的八进制表示(unsigned long long int
)
- 十六进制
%x
或%X
: 十六进制整数(小写或大写字母)(默认为int
)%lx
或%lX
: 无符号长整型的十六进制表示(unsigned long int
)%llx
或%llX
: 无符号长长整型的十六进制表示(unsigned long long int
)
浮点数类型
- 小数形式
%f
: 小数形式的浮点数(默认为double
)%lf
: 双精度浮点数(double
)%Lf
: 长双精度浮点数(long double
)
- 科学计数法
%e
或%E
: 科学计数法表示的浮点数(小写或大写字母'e')(默认为double
)%le
或%lE
: 双精度浮点数的科学计数法表示(double
)%Le
或%LE
: 长双精度浮点数的科学计数法表示(long double
)
- 自动选择格式
%g
或%G
: 根据数值大小自动选择%f
或%e
(小写或大写字母)(默认为double
)%lg
或%lG
: 双精度浮点数的自动选择格式(double
)%Lg
或%LG
: 长双精度浮点数的自动选择格式(long double
)
字符和字符串
%c
: 单个字符%s
: 字符串
指针
%p
: 指针地址
其他
%n
: 将当前已读取或写入的字符数量存入指向的整型变量中%%
: 输出百分号本身
精度和宽度控制
- 宽度控制
%5d
: 最少占用5个字符宽度,不足部分用空格填充(左对齐时加-
,如%-5d
)%05d
: 最少占用5个字符宽度,不足部分用0填充
- 精度控制
%.2f
: 小数点后保留2位小数%.3e
: 科学计数法表示,小数点后保留3位小数%.4g
: 自动选择格式,小数点后最多保留4位有效数字
事实上,这样的输入输出方式虽然有不少优点,但是也有不少弊端,因此我不是很推荐这种写法。
同步流对速度的影响
前面提到,C 风格的输入输出往往比 C++ 的更快,这是由于默认情况下 C++ 的输入输入输出需要与 C 风格的同步,借助于所谓同步流。我们可以通过关闭同步流解决这一问题:
std::cin.tie(nullptr)->sync_with_stdio(false);
只需要在 main
函数第一行添加上这句代码就可以了,此时 C++ 风格的速度往往与 C 风格的相同甚至更优。需要注意的是,使用这行代码后,你最好不要再使用 C 风格的输入输出,否则极有可能因为没有同步而导致出 bug。
实际上,这行代码不仅关闭了同步流,还解除了 std::cin
与 std::cout
的绑定,也能在一定程度上提升速度。
奇技淫巧:快读快写
你也许还学过所谓快读快写,使用 getchar
和 putchar
来优化输入输出,像下面这样:
int read() {
int x = 0, w = 1;
char ch = 0;
while (ch < '0' || ch > '9') { // ch 不是数字时
if (ch == '-') w = -1; // 判断是否为负
ch = getchar(); // 继续读入
}
while (ch >= '0' && ch <= '9') { // ch 是数字时
x = x * 10 + (ch - '0'); // 将新读入的数字「加」在 x 的后面
// x 是 int 类型,char 类型的 ch 和 '0' 会被自动转为其对应的
// ASCII 码,相当于将 ch 转化为对应数字
// 此处也可以使用 (x<<3)+(x<<1) 的写法来代替 x*10
ch = getchar(); // 继续读入
}
return x * w; // 数字 * 正负号 = 实际数值
}
void write(int x) {
static int sta[35];
int top = 0;
do { sta[top++] = x % 10, x /= 10; } while (x);
while (top) putchar(sta[--top] + 48); // 48 是 '0'
}
需要注意的是,putchar
与 getchar
都是 C 风格的,因此与关闭同步流的 C++ 风格输入输出不兼容。
当然了,还有更多复杂的技巧,但是个人不推荐使用也不推荐了解。这是由于在输入输出上花费这么多时间和精力得不偿失,只有毒瘤题会折腾输入输出。
文件读写之 freopen
freopen
可用于重定向输入输出到指定文件,使用格式如下:
freopen("test.in", "r", stdin);
freopen("test.out", "w", stdout);
将这两行代码添加到 main
函数开头就可以了。
这也是一个 C 风格的函数,因此不建议使用。优点是可以通过注释或取消注释这两行代码快速实现文件读写与控制台输入输出的切换。
文件读写之 fopen
这种文件读写也是 C 风格的,请自行阅读示例代码:
FILE *in_file = fopen("test.in", "r"),
*out_file = fopen("test.out", "w");
int number;
// 从输入文件读取整数
fscanf(in_file, "%d", &number);
// 写入输出文件
fprintf(out_file, "%d", number);
// 关闭文件
fclose(in_file);
fclose(out_file);
在竞赛中没什么优势,不推荐使用。
C++ 风格文件流
全文的重头戏来了!使用 C++ 风格的文件流进行文件读写不仅可以获得极致的速度,还能拥有舒适便捷的编码体验。只需要在 main
函数开头使用下面的代码打开文件:
std::ifstream fin("test.in");
std::ofstream fout("test.in");
接下来就可以使用 fin
与 fout
替换 std::cin
与 std::cout
进行文件读写:
int n;
fin >> n;
fout << n << '\n'; // 仍然不推荐使用 `std::endl` 用于换行
std::string str;
std::getline(fin, str);
char s[256];
fin.getline(s, 256);
当然了,你还可以通过添加下面的代码对 std::cin
与 std::cout
重定向:
std::cin.rdbuf(fin.rdbuf());
std::cout.rdbuf(fout.rdbuf());
std::cin.tie(nullptr)->sync_with_stdio(false);
这样就可以照常使用 std::cin
与 std::cout
进行文件读写了。同时可以通过注释或取消注释这两行代码实现文件读写与控制台输入输出的切换。有测试表明这种方法是不使用奇技淫巧的前提下的普遍最优读写方式。
C++ 风格格式化输入输出
以下是C++中常用的输入输出格式化控制方法示例:
// 1. 浮点数精度控制
double pi = 3.1415926535;
std::cout << std::fixed << std::setprecision(2) << pi << "\n"; // 输出3.14
std::cout << std::scientific << pi << "\n"; // 输出3.14e+00
// 2. 数值进制控制
int num = 255;
std::cout << std::hex << num << "\n"; // 输出ff
std::cout << std::oct << num << "\n"; // 输出377
std::cout << std::dec << num << "\n"; // 恢复十进制
// 3. 宽度与填充
std::cout << std::setw(10) << std::setfill('*') << 123 << "\n"; // 输出*******123
// 4. 对齐方式
std::cout << std::left << std::setw(10) << "Hello" << "\n"; // 左对齐
std::cout << std::right << std::setw(10) << "World" << "\n"; // 右对齐
// 5. 其他常用格式
std::cout << std::boolalpha << true << "\n"; // 输出true(而非1)
std::cout << std::showpos << 42 << "\n"; // 输出+42
std::cout << std::uppercase << 3.14e2 << "\n"; // 输出3.14E+02
// ========== 输入格式化 ==========
int year, month;
// 输入格式:YYYY/MM
std::cin >> year >> std::ws; // 跳过空白字符
std::cin.ignore(1); // 跳过分隔符'/'
std::cin >> month;
// 恢复默认设置
std::cout.unsetf(std::ios::fixed | std::ios::scientific);
std::cout.precision(6); // 恢复默认精度
常用格式化操作符对照表:
操作符/函数 | 作用 | 示例效果 |
---|---|---|
std::setprecision(n) |
设置浮点数精度 | 3.1416 → 3.14 |
std::fixed |
固定小数位显示 | 123.456 → 123.46 |
std::scientific |
科学计数法显示 | 1234.5 → 1.2345e+03 |
std::hex /std::oct |
十六进制/八进制显示 | 255 → ff / 377 |
std::setw(n) |
设置字段宽度 | 123 → " 123" |
std::setfill(c) |
设置填充字符 | 123 → "****123" |
std::left /std::right |
左对齐/右对齐 | "Hello" → "Hello " |
std::boolalpha |
布尔值文字显示 | true → "true" |
std::showpos |
显示正数符号 | 42 → +42 |
std::uppercase |
大写格式(与hex/scientific配合) | 0xff → 0XFF |
重要特性:
-
持久性设置:多数格式设置会保持生效,直到被显式修改
-
作用域控制:可通过
{}
限定格式作用范围{ std::ios_base::fmtflags f = std::cout.flags(); // 保存状态 std::cout << std::hex << 255; // 临时使用十六进制 std::cout.flags(f); // 恢复状态 }
-
性能影响:频繁切换格式设置可能影响I/O性能(建议批量处理)
对结构体添加输入输出支持
这部分相当简单,请看示例代码:
struct person {
std::string name;
int age;
friend std::istream &operator>>(std::istream &is, const person &p) {
is >> p.name >> p.age;
return is;
}
friend std::ostream &operator<<(std::ostream &os, const person &p) {
os << p.name << ": " << p.age;
return os;
}
};
person p;
std::cin >> p;
std::cout << p << '\n';