第二十四章:输入输出流——iostream、fstream、stringstream;格式化输入输出;文件读写模式
聊到C++的输入输出,很多人第一反应就是 cout << "Hello World"。嗯,没错,这是最基础的。但实际项目中,你面对的远不止这些。我做过不少数据处理的后端服务,每天跟文件、字符串、控制台打交道,输入输出流这块要是没吃透,调试起来能让你怀疑人生。
这一章,咱们就把 iostream、fstream、stringstream 这三个核心流讲透。顺便把格式化输入输出和文件读写模式也一并拿下。
24.1 三大流家族:谁负责什么?
C++ 的流库,说白了就是一套统一的接口。不管你是从键盘读、从文件读、还是从字符串读,操作方式都差不多。这就是“流”的思想——数据像水流一样,从源头流到目的地。
| 流类 | 头文件 | 用途 | 典型对象 |
|---|---|---|---|
istream / ostream | <iostream> | 标准输入输出(键盘/屏幕) | cin, cout, cerr, clog |
ifstream / ofstream | <fstream> | 文件读写 | 用户自定义对象 |
istringstream / ostringstream | <sstream> | 内存中的字符串读写 | 用户自定义对象 |
你看,结构非常对称。每个方向都有对应的输入流和输出流。我个人习惯把 stringstream 当作“内存里的临时缓冲区”,特别好用,后面会细说。
核心思想:所有流都继承自 ios_base,所以它们共享相同的格式化接口、状态标志、错误处理机制。学会一个,其他都触类旁通。
24.2 标准输入输出流:cin / cout / cerr / clog
这四个全局对象,你应该很熟悉了。但有几个细节,我敢说很多人没注意过。
24.2.1 cin 的陷阱
cin >> 默认以空白字符(空格、换行、制表符)作为分隔符。这意味着你输入 "Hello World",它只会读到 "Hello"。剩下的 "World" 还留在缓冲区里。
我曾经在做一个命令行交互工具时,就因为这个坑,导致后续的 cin >> 直接读到了残留数据,程序行为完全错乱。后来怎么解决的?用 cin.ignore() 清空缓冲区,或者改用 getline()。
#include <iostream>
#include <string>
using namespace std;
int main() {
string name;
int age;
cout << "请输入姓名: ";
getline(cin, name); // 读取一整行,包括空格
cout << "请输入年龄: ";
cin >> age;
// 注意:cin >> 后要清空换行符
cin.ignore(numeric_limits<streamsize>::max(), '\n');
cout << "姓名: " << name << ", 年龄: " << age << endl;
return 0;
}
小技巧:混合使用 cin >> 和 getline() 时,记得在 cin >> 后面加 cin.ignore()。否则 getline() 会直接读到残留的换行符,返回空字符串。
24.2.2 cerr 和 clog 的区别
很多人以为 cerr 和 clog 差不多,其实不然:
- cerr:无缓冲,立即输出到标准错误。适合紧急错误信息。
- clog:有缓冲,性能更好。适合日志信息。
我一般用 cerr 输出致命错误,用 clog 输出调试日志。这样在程序崩溃时,cerr 的内容不会丢失。
24.3 文件读写:fstream 实战
文件操作是日常开发中最常见的需求之一。C++ 提供了三种文件流:
ifstream:读文件ofstream:写文件fstream:读写文件(可同时)
24.3.1 文件打开模式
打开文件时,你可以指定模式。这些模式可以用位或 | 组合:
| 模式常量 | 含义 |
|---|---|
ios::in | 读模式 |
ios::out | 写模式(默认会清空文件) |
ios::app | 追加模式(写到文件末尾) |
ios::ate | 打开后定位到文件末尾 |
ios::binary | 以二进制模式打开 |
ios::trunc | 清空文件内容 |
举个例子,你想追加写入日志文件:
ofstream logFile("app.log", ios::app);
if (!logFile.is_open()) {
cerr << "无法打开日志文件!" << endl;
return -1;
}
logFile << "[" << __TIME__ << "] 用户登录成功" << endl;
logFile.close();
注意:默认的 ofstream 打开模式是 ios::out | ios::trunc。这意味着每次打开都会清空原有内容。如果你不想覆盖,记得加上 ios::app。
24.3.2 二进制读写
文本文件适合人类阅读,但二进制文件更高效、更精确。我曾在项目中用二进制格式存储传感器数据,读写速度比文本快了一个数量级。
#include <fstream>
using namespace std;
struct SensorData {
int id;
double value;
char status;
};
// 写入二进制文件
ofstream outFile("data.bin", ios::binary);
SensorData data = {1, 23.45, 'A'};
outFile.write(reinterpret_cast<const char*>(&data), sizeof(SensorData));
outFile.close();
// 读取二进制文件
ifstream inFile("data.bin", ios::binary);
SensorData readData;
inFile.read(reinterpret_cast<char*>(&readData), sizeof(SensorData));
inFile.close();
这里有个关键点:reinterpret_cast 把结构体指针转成 char*,因为流操作是按字节处理的。嗯,写起来有点啰嗦,但这是标准做法。
24.4 字符串流:stringstream 的妙用
stringstream 是我个人非常喜欢的一个工具。它让你像操作 cout 一样拼接字符串,像操作 cin 一样解析字符串。说白了,就是内存里的“虚拟文件”。
24.4.1 字符串拼接与格式化
#include <sstream>
#include <iostream>
using namespace std;
int main() {
ostringstream oss;
int id = 1001;
double price = 29.99;
string name = "C++编程";
oss << "商品ID: " << id << ", 名称: " << name << ", 价格: " << price;
string result = oss.str();
cout << result << endl;
return 0;
}
比起用 sprintf 或者手动拼接字符串,ostringstream 更安全、更直观。不用担心缓冲区溢出,也不用记格式化符号。
24.4.2 字符串解析
反过来,istringstream 可以把字符串拆解成各种类型:
string line = "42 3.14 hello";
istringstream iss(line);
int i;
double d;
string s;
iss >> i >> d >> s;
cout << "i=" << i << ", d=" << d << ", s=" << s << endl;
我曾经用这个技巧解析配置文件。每行读取后,扔进 istringstream 里按格式提取,代码非常干净。
24.5 格式化输入输出
C++ 提供了两种格式化方式:流操纵器(推荐)和 成员函数(老派)。我个人更推荐流操纵器,因为可以直接嵌入到 << 表达式中,代码更流畅。
24.5.1 常用流操纵器
| 操纵器 | 作用 |
|---|---|
dec / hex / oct | 设置整数进制(十进制/十六进制/八进制) |
setw(n) | 设置字段宽度为 n |
setprecision(n) | 设置浮点数精度(有效数字或小数位数) |
fixed / scientific | 固定小数点表示 / 科学计数法 |
boolalpha / noboolalpha | bool 值输出为 true/false 或 1/0 |
left / right / internal | 对齐方式 |
setfill(c) | 设置填充字符 |
#include <iostream>
#include <iomanip> // 必须包含这个头文件
using namespace std;
int main() {
double pi = 3.1415926535;
int num = 255;
cout << "默认: " << pi << endl;
cout << fixed << setprecision(3) << "保留3位小数: " << pi << endl;
cout << "十六进制: " << hex << num << endl;
cout << "宽度10, 左对齐, 填充'*': " << left << setw(10) << setfill('*') << num << endl;
return 0;
}
提示:setw() 只对下一次输出有效,其他操纵器是“粘性的”,会一直生效直到被改变。我刚开始用的时候经常忘记这一点,导致后续输出格式全乱了。
24.5.2 成员函数方式
如果你不想用操纵器,也可以用流的成员函数:
cout.setf(ios::fixed | ios::showpoint);
cout.precision(4);
cout << 123.456789 << endl; // 输出 123.4568
两种方式效果一样。我个人觉得操纵器更直观,但成员函数在某些需要动态控制格式的场景下更灵活。
24.6 流状态与错误处理
流操作不是永远成功的。文件可能打不开,输入可能格式错误。C++ 流提供了状态标志:
good():一切正常eof():到达文件末尾fail():上次操作失败(比如类型不匹配)bad():流已损坏(不可恢复)
我习惯这样检查:
ifstream file("data.txt");
if (!file) {
cerr << "文件打开失败!" << endl;
return;
}
int value;
while (file >> value) {
// 处理数据
}
if (file.eof()) {
cout << "读取完成" << endl;
} else if (file.fail()) {
cerr << "数据格式错误!" << endl;
}
注意 while (file >> value) 这个写法。它利用了流的 operator bool,当读取成功时返回 true,遇到错误或 EOF 时返回 false。简洁又安全。
24.7 避坑指南
最后,分享几个我踩过的坑:
- 忘记关闭文件:虽然析构函数会自动关闭,但显式
close()是好习惯。尤其是在写日志时,不关闭可能导致数据丢失。 - 二进制文件跨平台问题:不同平台对换行符的处理不同(Windows 是
\r\n,Linux 是\n)。二进制模式ios::binary可以避免这种转换。 - 流状态未重置:如果流进入
fail状态,后续所有操作都会失败。记得调用clear()重置状态。 - getline 与 cin 混用:前面说过了,
cin >>后要ignore(),否则getline读到空行。
好了,这一章的内容就到这里。输入输出流看似基础,但用好它能让你的代码更健壮、更优雅。下次写文件或解析字符串时,试试 stringstream,你会爱上它的。