前言
只要包含<iostream>,就能直接使用std::cin、std::cout、std::cerr、std::clog,以及对应的宽字符版本std::wcin、std::wcout、std::wcerr、std::wclog。这八个对象由标准库在程序启动时构造,不需要用户定义。它们看起来像「语言自带的输入输出」,实际是全局对象,行为由流缓冲、绑定关系、同步设置共同决定。
常见误解集中在三处。第一,把std::cerr和std::clog当成完全一样:两者都写到标准错误,但cerr设置了unitbuf,每次输出后立即刷新;clog是带缓冲的,刷新时机由缓冲区和flush决定。第二,以为std::endl只是「换行」:它还强制刷新缓冲区,在大量输出的循环里频繁使用会显著增加系统调用次数——本文不给具体倍数,但这是可以通过测量验证的结构性差异。第三,以为std::cin和std::cout互不相关:默认情况下std::cin绑定在std::cout上,读取前会自动刷新cout,这正是「先提示、再等待输入」能正常工作的原因。
本文按「对象清单、缓冲与刷新、绑定与同步、重定向、完整示例」展开,代码基准为 C++17。
一、八个预定义对象
| 对象 | 类型 | 目标 | 默认缓冲行为 |
|---|
std::cin | std::istream | 标准输入 | 与 C 的stdin同步 |
std::cout | std::ostream | 标准输出 | 缓冲,遇换行不自动刷新 |
std::cerr | std::ostream | 标准错误 | unitbuf,每次输出后刷新 |
std::clog | std::ostream | 标准错误 | 缓冲 |
std::wcin | std::wistream | 标准输入(宽) | 同上,宽字符 |
std::wcout | std::wostream | 标准输出(宽) | 同上,宽字符 |
std::wcerr | std::wostream | 标准错误(宽) | 同上,宽字符 |
std::wclog | std::wostream | 标准错误(宽) | 同上,宽字符 |
这些对象在<iostream>中声明为extern,定义在标准库的某个翻译单元里。标准保证它们在main执行前已经构造完成,并且在main返回后仍然可用(用于静态对象析构期间的输出)。这也是为什么在全局对象的析构函数里写std::cout通常是安全的,而写自己定义的全局日志对象则可能遭遇「对象已销毁」。
std::cerr的unitbuf标志可以用std::cerr.flags()观察,但不要依赖具体位模式;标准只规定它「每次输出后刷新」。std::cerr还默认与std::cout绑定(tie),所以写cerr前会先刷新cout,避免提示信息与错误信息顺序错乱。
宽字符流与窄字符流是两套独立的缓冲。混用std::cout和std::wcout时,输出顺序不保证符合调用顺序;在同一个程序里最好只用一套。
二、缓冲、刷新与 endl
流输出的终点是缓冲区,不是立即写到文件描述符。缓冲的意义是合并小写入,减少系统调用。刷新的方式有三种:
- 缓冲区满时自动刷新;
- 显式调用
flush()或写入std::flush; - 写入
std::endl(换行并刷新)或std::ends(写入空字符并刷新)。
// 适用于 C++17
#include <iostream>
void demo_flush() {
std::cout << "no flush" << '\n'; // 只换行
std::cout << "with flush" << std::endl; // 换行并刷新
std::cout.flush(); // 仅刷新
}'\n'与std::endl的区别只在是否刷新,换行效果相同。循环里输出大量行时,用'\n'让缓冲区自行合并,通常比每行std::endl少很多次写操作。需要确保「崩溃前日志已经落盘」时,才值得逐行刷新;而std::cerr已经默认逐次刷新,适合错误信息。
std::clog是带缓冲的错误流,适合不希望每次错误都触发系统调用的场景。它和std::cerr都指向标准错误,重定向2>会同时捕获两者。
程序正常退出时,标准流的缓冲区会被刷新;但std::abort()、std::_Exit()或未捕获的异常导致std::terminate时,不保证刷新。需要「最后一条日志一定写出」的场景,应在写完后显式flush()。
三、绑定与同步:tie 与 sync_with_stdio
cin 与 cout 的绑定
std::cin默认通过tie()指向std::cout。每次对cin进行格式化读取前,流会先刷新被绑定的输出流。这样下面这段代码不会出现「提示还没显示就卡在输入」的情况:
// 适用于 C++17
#include <iostream>
#include <string>
void ask_name() {
std::cout << "name: "; // 没有换行,也没有 flush
std::string name;
std::cin >> name; // 读取前自动刷新 cout
}如果确定不需要这种自动刷新(例如已经手动管理刷新,或输入输出完全分离),可以解除绑定:
// 适用于 C++17
std::cin.tie(nullptr);解除后,std::cout << "name: ";之后必须自己flush(),否则提示可能留在缓冲区里。
sync_with_stdio
默认情况下,C++ 流与 C 的stdio是同步的:std::cout与printf可以混用,输出顺序符合调用顺序。代价是 C++ 流在底层要多做一些协调工作。调用
// 适用于 C++17
std::ios_base::sync_with_stdio(false);可以关闭同步,让 C++ 流使用自己的缓冲。这个调用必须在任何标准流输入输出之前执行,通常放在main开头。关闭同步后再混用printf与std::cout,输出顺序不再保证;这是标准明确的状态,不是实现缺陷。
很多教程把关闭同步描述成「加速输入输出」的万能手段,这是过度简化。它改变的是同步开销与缓冲策略,实际效果取决于调用模式、数据量和标准库实现,应当测量而不是照搬。同样,std::cin.tie(nullptr)与sync_with_stdio(false)经常一起出现,但它们是两个独立开关,各自解决不同问题。
四、重定向与格式化
用 rdbuf 重定向
每个流对象都持有一个流缓冲指针,rdbuf()可以读取或替换它。把std::cout的缓冲换成文件流的缓冲,就能让后续所有std::cout输出写入文件:
// 适用于 C++17
#include <fstream>
#include <iostream>
void redirect_demo() {
std::ofstream file("output.txt");
std::streambuf* original = std::cout.rdbuf(file.rdbuf());
std::cout << "this goes to the file\n";
std::cout.flush();
std::cout.rdbuf(original); // 必须恢复
std::cout << "this goes to the console\n";
}rdbuf返回旧的缓冲指针,务必保存并在结束后恢复。若中途抛异常而没有恢复,后续输出会继续写入文件,且file析构后std::cout持有的缓冲指针悬垂——这是未定义行为。稳妥写法是用 RAII 包装一个「退出时恢复」的小对象。
这种重定向只影响 C++ 流,不影响printf(除非同步仍然开启且实现把它们关联起来,这一点不应依赖)。进程级的重定向应当用操作系统的freopen或 shell 的>。
格式化状态是持久的
流对象保存着一组格式标志:进制、浮点记法、精度、宽度、填充字符、对齐方式。修改后对后续所有输出生效,直到再次修改:
// 适用于 C++17
#include <iomanip>
#include <iostream>
void format_demo() {
std::cout << std::hex << 255 << '\n'; // ff
std::cout << std::dec << 255 << '\n'; // 255
std::cout << std::boolalpha << true << '\n'; // true
std::cout << std::noboolalpha << true << '\n'; // 1
std::cout << std::fixed << std::setprecision(2) << 3.14159 << '\n';
}std::setw是例外:它只对下一次输出生效,之后自动重置。宽度、填充、对齐常常一起使用,写表格时要注意setw的一次性语义。std::setprecision在没有fixed时表示有效数字位数,加上fixed后表示小数点后的位数,这个区别经常被忽略。
格式标志的持久性意味着「一段代码改了格式,另一段代码遭殃」。在库函数里输出前保存flags()、precision()、fill(),结束后恢复,或者使用局部的std::ostringstream拼好再一次性输出。
五、完整示例
下面的程序把提示、正常结果、错误信息分别送到cout与cerr,演示clog的缓冲差异、tie的作用与格式恢复:
// 适用于 C++17
#include <iomanip>
#include <iostream>
#include <string>
struct Report {
std::string name;
double value = 0.0;
bool valid = false;
};
Report parse(const std::string& line) {
Report report;
const std::size_t space = line.find(' ');
if (space == std::string::npos) {
return report;
}
report.name = line.substr(0, space);
try {
report.value = std::stod(line.substr(space + 1));
report.valid = true;
} catch (const std::exception&) {
report.valid = false;
}
return report;
}
int main() {
std::cout << "enter: name value\n";
std::string line;
while (std::getline(std::cin, line)) {
const Report report = parse(line);
if (!report.valid) {
std::cerr << "bad line: " << line << '\n'; // 立即刷新
std::clog << "bad line logged\n"; // 缓冲
continue;
}
const auto old_flags = std::cout.flags();
const auto old_precision = std::cout.precision();
std::cout << std::left << std::setw(12) << report.name
<< std::right << std::fixed << std::setprecision(2)
<< report.value << '\n';
std::cout.flags(old_flags);
std::cout.precision(old_precision);
}
std::cout.flush(); // 确保最后一行写出
}几个细节值得对照。std::getline读取整行,允许名字和值之间出现空格以外的内容;std::stod在无法转换时抛std::invalid_argument,示例统一捕获std::exception并标记失败。输出前保存flags()与precision(),结束后恢复,避免这段代码改变全局的格式状态。std::setw(12)只对紧随其后的report.name生效,下一次输出不再有宽度限制。
std::cerr与std::clog都写到标准错误,但前者每次插入后刷新,后者留在缓冲区。若程序随后崩溃,clog的内容可能丢失;若错误量很大,cerr的系统调用次数会更多。选择哪一个,取决于「可靠性」与「开销」的权衡,而不是习惯。
一个容易忽略的行为:while (std::getline(std::cin, line))结束时,std::cin处于eofbit状态。如果后面还要继续读标准输入,必须先std::cin.clear()。不要把「流结束」与「流损坏」混为一谈:eof()为真但bad()为假是正常结束,fail()为真且eof()为假通常意味着格式不匹配。
常见坑点
- ❌ 认为
std::endl只是换行,在循环里大量使用。
✅std::endl会刷新缓冲区;只换行用'\n',需要落盘时再flush()。
- ❌ 认为
std::cerr与std::clog完全等价。
✅cerr是unitbuf每次刷新,clog带缓冲;可靠性与开销不同。
- ❌ 关闭
sync_with_stdio(false)后继续混用printf与std::cout。
✅ 关闭同步后两者顺序不保证;要么只用一套,要么保持同步。
- ❌ 在第一次输入输出之后才调用
sync_with_stdio(false)。
✅ 必须在任何标准流操作之前调用,通常放在main开头。
- ❌ 用
rdbuf()重定向后忘记恢复。
✅ 保存返回的旧缓冲指针并在结束时恢复;异常路径用 RAII 保证恢复。
- ❌ 以为
std::cin读取前不会刷新std::cout。
✅ 默认cin绑定cout,读取前自动刷新;cin.tie(nullptr)会解除。
- ❌ 修改
std::cout的格式后不恢复,影响后续输出。
✅ 保存flags()、precision()、fill()并在结束后恢复;setw本身只生效一次。
- ❌ 认为多线程同时
std::cout <<能保证整行原子输出。
✅ 标准流并发访问不构成数据竞争,但字符可能交错;需要整行完整时自行加锁。
总结
| 主题 | 结论 |
|---|
| 对象数量 | 4 个窄字符加 4 个宽字符,共 8 个预定义流对象 |
cout | 缓冲,'\n'不刷新 |
cerr | 标准错误,unitbuf,每次输出后刷新 |
clog | 标准错误,带缓冲 |
endl | 换行并刷新,不等于'\n' |
| 绑定 | cin默认绑定cout,读取前刷新 |
| 同步 | sync_with_stdio(false)必须最先调用 |
| 重定向 | rdbuf()替换缓冲,务必恢复 |
| 格式状态 | 持久生效,setw除外 |
预定义流对象用起来简单,行为却由缓冲、绑定、同步、格式状态四套机制共同决定。写日志时先想清楚「这条信息丢失的代价有多大」,再决定用cerr还是clog、用endl还是'\n';写库函数时先想清楚「我会不会污染调用方的格式状态」,再决定保存与恢复。把这些默认行为当成接口契约的一部分,而不是可以忽略的细节,输出相关的诡异问题会少很多。