引言
在 C 语言中,文件操作依赖于 FILE* 和一系列函数(fopen、fread、fwrite、fprintf 等)。这种方式虽然功能齐全,但存在类型不安全、容易忘记关闭文件、错误处理繁琐等问题。
C++ 引入了**流(Stream)**的概念,将输入输出抽象为"数据流",通过统一的接口操作不同的设备(键盘、屏幕、文件、字符串等)。流体系基于面向对象设计,提供了类型安全、可扩展的 IO 操作方式。

第一部分:IO 流类层次体系
一、完整继承关系

二、常用头文件与类
| 头文件 | 包含的类 | 用途 |
|---|---|---|
<iostream> |
cin, cout, cerr, clog |
标准控制台 IO |
<fstream> |
ifstream, ofstream, fstream |
文件 IO |
<sstream> |
istringstream, ostringstream, stringstream |
字符串 IO |
<iomanip> |
操纵符函数(setw, setprecision 等) |
格式控制 |
三、四个标准流对象
cpp
#include <iostream>
using namespace std;
// cin --- istream 对象,关联标准输入(键盘)
// cout --- ostream 对象,关联标准输出(屏幕)
// cerr --- ostream 对象,关联标准错误(无缓冲)
// clog --- ostream 对象,关联标准日志(有缓冲)
cerr 和 clog 的区别:
cpp
cerr << "错误信息" << endl; // 立即输出(无缓冲)
clog << "日志信息" << endl; // 缓冲后输出
第二部分:标准输入流详解
一、逐字符读取
cpp
#include <iostream>
using namespace std;
int main() {
char ch;
// 方式1:get() 获取一个字符(返回 istream 引用)
cin.get(ch);
// 方式2:get() 返回字符的 ASCII 值(int 类型)
int ch2 = cin.get();
cout << ch << ", " << ch2 << endl;
return 0;
}
get() 的两种重载对比:
| 重载形式 | 返回值 | 用途 |
|---|---|---|
cin.get(ch) |
istream& |
读取字符存入 ch |
cin.get() |
int |
返回读取字符的 ASCII 值(-1 表示 EOF) |
二、逐行读取
cpp
char buf[128] = {0};
// 方式1:getline(),遇到换行符结束
cin.getline(buf, 128);
// 方式2:getline() 指定分隔符
cin.getline(buf, 128, '\n'); // 第三个参数是分隔符
// 方式3:read() 读取指定字节数
cin.read(buf, 10);
getline() 与 >> 的区别:
| 方法 | 遇到空格 | 遇到换行 | 读取换行符 | 安全性 |
|---|---|---|---|---|
cin >> buf |
停止 | 停止 | 不读取(留在缓冲区) | ❌ 无长度限制 |
cin.getline(buf, n) |
继续 | 停止 | 读取并丢弃 | ✅ 有长度限制 |
三、自定义读行函数
cpp
#include <string>
// C 风格字符串读取
long readline(char* buf, int maxSize) {
char ch;
int len = 0;
while (true) {
cin.get(ch);
if (ch == '\n' || len >= maxSize) break;
buf[len++] = ch;
}
return len;
}
// C++ string 读取
long readString(string& s) {
char ch;
while (true) {
cin.get(ch);
if (ch == '\n') break;
s += ch;
}
return s.size();
}
四、缓冲区管理
cpp
cin.ignore(); // 忽略一个字符(清空缓冲区)
cin.ignore(100); // 忽略最多 100 个字符
cin.ignore(100, '\n'); // 忽略直到换行符(最多 100 个)
cin.clear(); // 清除错误状态标志
常见场景 :cin >> n 后残留的换行符需要清理
cpp
int n;
cin >> n;
cin.ignore(); // 清除残留的 '\n'
// 然后安全读取下一行
string line;
getline(cin, line);
第三部分:标准输出流详解
一、基本输出方法
cpp
// put() --- 输出单个字符
cout.put('A');
cout.put('\n');
// write() --- 输出指定长度的字符串
cout.write("hello", 5); // 输出5个字符
// flush() --- 强制刷新缓冲区
cout.flush();
// endl --- 输出换行并刷新
cout << endl; // 等价于 cout << '\n' << flush;
输出缓冲区的四种刷新时机:
| 刷新模式 | 触发条件 |
|---|---|
| 满刷新 | 缓冲区满时自动刷新 |
| 行刷新 | 遇到换行符 \n 时刷新 |
| 程序退出 | exit() 或 return 时刷新 |
| 强制刷新 | 调用 flush() 或 endl |
二、格式控制 --- 进制输出
cpp
#include <iomanip>
int n = 100;
// 使用操纵符
cout << showbase << hex << n << endl; // 0x64 (十六进制,带前缀)
cout << oct << n << endl; // 0144 (八进制)
cout << dec << n << endl; // 100 (十进制)
// showbase --- 显示进制前缀(0x 或 0)
// noshowbase --- 取消进制前缀
| 操纵符 | 效果 |
|---|---|
hex |
十六进制输出 |
oct |
八进制输出 |
dec |
十进制输出(默认) |
showbase |
显示进制前缀 |
noshowbase |
取消进制前缀 |
uppercase |
十六进制字母大写 |
nouppercase |
十六进制字母小写(默认) |
三、格式控制 --- 使用 flags
cpp
// 获取当前标志
ios::fmtflags old_flags = cout.flags();
// 设置新标志(会覆盖旧标志)
cout.flags(ios::showbase | ios::hex);
cout << 90 << endl; // 0x5a
// 恢复旧标志
cout.flags(old_flags);
常用格式标志:
| 标志 | 含义 |
|---|---|
ios::showbase |
显示进制前缀 |
ios::hex |
十六进制 |
ios::oct |
八进制 |
ios::dec |
十进制 |
ios::left |
左对齐 |
ios::right |
右对齐 |
ios::fixed |
固定小数位 |
ios::scientific |
科学计数法 |
四、格式控制 --- 宽度和填充
cpp
cout.width(20); // 设置输出宽度为 20(只影响下一个输出)
cout.fill('*'); // 设置填充字符为 '*'
cout << left; // 左对齐
cout << "hi,disen!" << endl;
// 输出:hi,disen!***********
// 使用操纵符
cout << setw(20) << setfill('*') << left << "hi,disen!" << endl;
| 方法/操纵符 | 作用 | 生效范围 |
|---|---|---|
width(n) / setw(n) |
设置输出宽度 | 只影响下一个输出 |
fill(c) / setfill(c) |
设置填充字符 | 持久生效 |
left |
左对齐 | 持久生效 |
right |
右对齐 | 持久生效 |
五、格式控制 --- 浮点数精度
cpp
#include <iomanip>
double d = 1.2345678;
// 设置精度
cout << setprecision(3) << d << endl; // 1.23
cout << setprecision(1) << 2.459 << endl; // 2
// 固定小数位模式
cout << fixed << setprecision(2) << 1.345678 << endl; // 1.35
// 科学计数法模式
cout << scientific << 123.456 << endl; // 1.234560e+02
// 默认模式
cout << defaultfloat << 99.2389 << endl;
| 操纵符 | 精度含义 |
|---|---|
defaultfloat |
有效数字位数(默认) |
fixed |
小数点后位数 |
scientific |
小数点后位数(科学计数法) |
第四部分:文件流详解
一、文件打开模式
cpp
#include <fstream>
// 定义在 ios 中
ios::in // 读模式(ifstream 默认)
ios::out // 写模式(ofstream 默认)
ios::app // 追加模式(写入到文件末尾)
ios::ate // 打开时定位到文件末尾
ios::trunc // 打开时清空文件内容
ios::binary // 二进制模式
组合使用:
cpp
// 读写模式,二进制
fstream fs("data.dat", ios::in | ios::out | ios::binary);
// 写模式,追加
ofstream fs("log.txt", ios::out | ios::app);
二、文件写入
cpp
#include <fstream>
#include <cstring>
int main() {
ofstream fs("a.txt", ios::out);
if (!fs.good()) {
cout << "打开文件失败" << endl;
return -1;
}
char line[128] = {0};
while (true) {
cin.getline(line, 128);
if (strlen(line) == 0) break;
fs.write(line, strlen(line));
fs.write("\n", 1);
}
fs.close();
return 0;
}
三、文件读取
cpp
#include <fstream>
#include <string>
int main() {
ifstream fs("a.txt"); // 默认 ios::in
if (!fs.good()) return -1;
// 方式1:逐行读取
string line;
while (getline(fs, line)) {
cout << line << endl;
}
// 方式2:逐词读取
string word;
while (fs >> word) {
cout << word << endl;
}
fs.close();
return 0;
}
注意 :代码中 while (!fs.eof()) 存在一个常见陷阱:
cpp
// ❌ 有问题的写法
while (!fs.eof()) {
fs.getline(buf, 128);
cout << buf << endl; // 可能输出两次最后一行
}
// ✓ 正确的写法(将读取操作放在循环条件中)
while (fs.getline(buf, 128)) {
cout << buf << endl;
}
原理 :eof() 只在尝试读取失败后才变为 true,不是"预知"文件结束。
四、二进制文件读写
cpp
struct Person {
int pid;
char name[32];
void hi() {
cout << "pid: " << pid << ", name: " << name << endl;
}
};
int main() {
Person p1{1001, "Lucy"}, p2{1002, "Disen"};
// 二进制写入
fstream fs("b.dat", ios::out | ios::binary);
if (!fs.good()) return -1;
fs.write(reinterpret_cast<char*>(&p1), sizeof(Person));
fs.write(reinterpret_cast<char*>(&p2), sizeof(Person));
fs.close();
// 二进制读取 --- 先获取文件大小
fstream ifs("b.dat", ios::in | ios::binary);
if (!ifs.good()) return -1;
ifs.seekg(0, ios::end); // 移到文件末尾
auto len = ifs.tellg(); // 获取当前位置(即文件大小)
int n = len / sizeof(Person);
cout << "Person 个数: " << n << endl;
ifs.seekg(0, ios::beg); // 移回文件开始
for (int i = 0; i < n; i++) {
Person p;
ifs.read(reinterpret_cast<char*>(&p), sizeof(Person));
p.hi();
}
ifs.close();
return 0;
}
五、文件流状态检查
cpp
ifstream fs("test.txt");
// 方法1:good()
if (fs.good()) {
// 文件打开成功,且没有错误
}
// 方法2:is_open()
if (fs.is_open()) {
// 文件已成功打开
}
// 方法3:直接用作布尔值
if (!fs) {
// 文件打开失败
}
| 状态函数 | 含义 |
|---|---|
good() |
流状态正常,无任何错误 |
eof() |
到达文件末尾 |
fail() |
操作失败(可恢复) |
bad() |
严重错误(不可恢复) |
is_open() |
文件是否打开 |
六、文件指针定位
cpp
ifstream fs("data.txt");
// seekg() --- 移动读指针
fs.seekg(0, ios::beg); // 移到开头
fs.seekg(0, ios::end); // 移到末尾
fs.seekg(10, ios::cur); // 从当前位置后移10字节
// tellg() --- 获取读指针位置
auto pos = fs.tellg();
// seekp() --- 移动写指针(ofstream)
// tellp() --- 获取写指针位置
| 定位标志 | 含义 |
|---|---|
ios::beg |
相对于文件开头 |
ios::cur |
相对于当前位置 |
ios::end |
相对于文件末尾 |
第五部分:C 与 C++ IO 对照表
| 操作 | C 语言 | C++ 流 |
|---|---|---|
| 打开文件 | fopen("a.txt", "r") |
ifstream fs("a.txt") |
| 关闭文件 | fclose(fp) |
fs.close() 或析构自动 |
| 读字符 | fgetc(fp) |
fs.get(ch) |
| 读一行 | fgets(buf, n, fp) |
fs.getline(buf, n) |
| 写字符 | fputc(ch, fp) |
fs.put(ch) |
| 写一行 | fputs(str, fp) |
fs << str |
| 二进制读 | fread(buf, sz, n, fp) |
fs.read(buf, n) |
| 二进制写 | fwrite(buf, sz, n, fp) |
fs.write(buf, n) |
| 文件指针 | fseek(fp, 0, SEEK_END) |
fs.seekg(0, ios::end) |
| 获取位置 | ftell(fp) |
fs.tellg() |
| 错误检查 | 检查返回值 | fs.good() / !fs |
| 格式化输出 | fprintf(fp, "%x", n) |
fs << hex << n |
总结
一、IO 流体系核心类

二、文件操作通用流程
- 创建流对象
ifstream fs("filename", ios::in);
- 检查是否打开成功
if (!fs.good()) { /* 错误处理 */ }
- 读取/写入操作
fs >> data; 或 fs.read(buf, size);
- 关闭文件
fs.close();
// 或者依赖析构函数自动关闭
三、关键记忆点
| 要点 | 说明 |
|---|---|
cin.ignore() |
清除缓冲区残留 |
cout.width(n) |
只影响下一个输出 |
cout.fill(c) |
持久生效 |
while (getline(fs, line)) |
正确的逐行读取方式 |
reinterpret_cast<char*>(&obj) |
二进制读写结构体时的类型转换 |
fs.seekg(0, ios::end) |
计算文件大小 |
| RAII 特性 | 流对象析构时自动关闭文件 |
C++ IO 流体系是一个设计精巧的面向对象框架,它将控制台、文件、字符串等不同设备的输入输出统一到一个继承层次中。理解流体系的关键在于:
-
继承层次 :
istream/ostream是所有输入输出流的基类 -
格式控制:通过操纵符(manipulator)控制输出格式
-
RAII 机制:流对象析构时自动关闭文件,避免资源泄漏
-
状态检查 :始终检查
good()或直接使用流对象的布尔值判断