第二十四章:输入输出流——iostream、fstream、stringstream;格式化输入输出;文件读写模式

聊到C++的输入输出,很多人第一反应就是 cout << "Hello World"。嗯,没错,这是最基础的。但实际项目中,你面对的远不止这些。我做过不少数据处理的后端服务,每天跟文件、字符串、控制台打交道,输入输出流这块要是没吃透,调试起来能让你怀疑人生。

这一章,咱们就把 iostreamfstreamstringstream 这三个核心流讲透。顺便把格式化输入输出和文件读写模式也一并拿下。

24.1 三大流家族:谁负责什么?

C++ 的流库,说白了就是一套统一的接口。不管你是从键盘读、从文件读、还是从字符串读,操作方式都差不多。这就是“流”的思想——数据像水流一样,从源头流到目的地。

流类头文件用途典型对象
istream / ostream<iostream>标准输入输出(键盘/屏幕)cin, cout, cerr, clog
ifstream / ofstream<fstream>文件读写用户自定义对象
istringstream / ostringstream<sstream>内存中的字符串读写用户自定义对象

你看,结构非常对称。每个方向都有对应的输入流和输出流。我个人习惯把 stringstream 当作“内存里的临时缓冲区”,特别好用,后面会细说。

核心思想:所有流都继承自 ios_base,所以它们共享相同的格式化接口、状态标志、错误处理机制。学会一个,其他都触类旁通。

C++ 输入输出流体系 ios_base istream ostream iostream ifstream istringstream ofstream ostringstream fstream cin / cout / cerr / clog 是预定义的全局对象 文件流和字符串流需要自己创建对象

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 的区别

很多人以为 cerrclog 差不多,其实不然:

  • 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 / noboolalphabool 值输出为 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,你会爱上它的。


公众号:蓝海资料掘金营,微信deep3321