引言
在 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等) | 格式控制 |
三、四个标准流对象
#include <iostream> using namespace std; // cin — istream 对象,关联标准输入(键盘) // cout — ostream 对象,关联标准输出(屏幕) // cerr — ostream 对象,关联标准错误(无缓冲) // clog — ostream 对象,关联标准日志(有缓冲)cerr 和 clog 的区别:
cerr << "错误信息" << endl; // 立即输出(无缓冲) clog << "日志信息" << endl; // 缓冲后输出第二部分:标准输入流详解
一、逐字符读取
#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) |
二、逐行读取
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) | 继续 | 停止 | 读取并丢弃 | ✅ 有长度限制 |
三、自定义读行函数
#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(); }四、缓冲区管理
cin.ignore(); // 忽略一个字符(清空缓冲区) cin.ignore(100); // 忽略最多 100 个字符 cin.ignore(100, '\n'); // 忽略直到换行符(最多 100 个) cin.clear(); // 清除错误状态标志常见场景:cin >> n后残留的换行符需要清理
int n; cin >> n; cin.ignore(); // 清除残留的 '\n' // 然后安全读取下一行 string line; getline(cin, line);第三部分:标准输出流详解
一、基本输出方法
// 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 |
二、格式控制 — 进制输出
#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
// 获取当前标志 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 | 科学计数法 |
四、格式控制 — 宽度和填充
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 | 右对齐 | 持久生效 |
五、格式控制 — 浮点数精度
#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 | 小数点后位数(科学计数法) |
第四部分:文件流详解
一、文件打开模式
#include <fstream> // 定义在 ios 中 ios::in // 读模式(ifstream 默认) ios::out // 写模式(ofstream 默认) ios::app // 追加模式(写入到文件末尾) ios::ate // 打开时定位到文件末尾 ios::trunc // 打开时清空文件内容 ios::binary // 二进制模式组合使用:
// 读写模式,二进制 fstream fs("data.dat", ios::in | ios::out | ios::binary); // 写模式,追加 ofstream fs("log.txt", ios::out | ios::app);二、文件写入
#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; }三、文件读取
#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())存在一个常见陷阱:
// ❌ 有问题的写法 while (!fs.eof()) { fs.getline(buf, 128); cout << buf << endl; // 可能输出两次最后一行 } // ✓ 正确的写法(将读取操作放在循环条件中) while (fs.getline(buf, 128)) { cout << buf << endl; }原理:eof()只在尝试读取失败后才变为 true,不是"预知"文件结束。
四、二进制文件读写
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; }五、文件流状态检查
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() | 文件是否打开 |
六、文件指针定位
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 流体系核心类
二、文件操作通用流程
1. 创建流对象
ifstream fs("filename", ios::in);2. 检查是否打开成功
if (!fs.good()) { /* 错误处理 */ }3. 读取/写入操作
fs >> data; 或 fs.read(buf, size);4. 关闭文件
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()或直接使用流对象的布尔值判断