文件与流
原理
流的缓冲区三层架构
C++ IO 流采用三层架构,将用户程序的格式化和底层系统调用解耦:
用户程序层: ofstream / ifstream / cout ← 格式化 >> / << / getline
缓冲区层: streambuf / filebuf ← 内部字符数组,减少系统调用
系统调用层: read() / write() (POSIX) ← 用户态→内核态切换
输出流程
ofstream << "hello" 的数据流经路径:
- 数据写入 streambuf 的内部缓冲区
- 缓冲区满(或显式
flush、遇到endl、程序正常结束)时 - 通过
write()系统调用一次性写入内核 - 内核将数据写入磁盘(可能通过页缓存进一步延迟)
读取流程
ifstream >> value 的数据流经路径:
- 内核通过
read()从磁盘读取一大块数据(如 4KB) - 放入 streambuf 的缓冲区
- 用户程序从缓冲区逐字节/逐行取数据
- 缓冲区耗尽时再次触发
read()
三种缓冲策略
| 策略 | 触发条件 | 典型流 |
|---|---|---|
| 全缓冲 | 缓冲区满才执行系统调用 | ofstream(默认 4KB 或更大) |
| 行缓冲 | 遇到 \n 时刷新 | cout(连接终端时) |
| 无缓冲 | 每次写操作立即系统调用 | cerr(保证错误立即输出) |
endl=\n+flush。在不需要立即刷新缓冲区的场景(性能敏感),应使用\n而非endl。
文件描述符与内核文件表
每个 C++ fstream 对象内部关联一个文件描述符(fd)。fd 是进程文件描述符表中的索引,指向内核级”打开文件表”中的一项。该表项记录当前文件偏移量、打开模式和 inode 指针。
进程 fd 表 内核打开文件表 inode
┌──────┐ ┌──────────────┐ ┌────────┐
│ 0: STDIN │ │ offset: 1024 │ │ size │
│ 1: STDOUT│ │ mode: RDONLY │───→│ blocks │
│ 3: fd ───┼─────→│ inode ptr │ └────────┘
└──────┘ └──────────────┘
sync_with_stdio
默认情况下 C++ 流与 C 标准库的 stdin/stdout/stderr 保持同步(ios_base::sync_with_stdio(true)),允许混合使用 cout 和 printf。同步的代价是每次 IO 操作要刷新双边缓冲区。如果程序只用 C++ 流,可关闭同步以获性能提升:
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);二进制模式的实质
二进制模式(ios::binary)在 Linux 上与文本模式无区别——因为 Linux 使用 \n 换行。在 Windows 上,文本模式会自动将 \n 转换为 \r\n,二进制模式不做此转换。二进制读写不进行任何字符转换,直接操作字节。
语法
文件流
std::ofstream out("file.txt"); // 写(默认 ios::out | ios::trunc)
std::ofstream app("file.txt", std::ios::app); // 追加
std::ifstream in("file.txt"); // 读(默认 ios::in)
std::fstream io("file.txt", std::ios::in | std::ios::out); // 读写
if (!in.is_open()) { /* 打开失败 */ }流状态
stream.good(); // 无错误
stream.eof(); // 到达文件尾
stream.fail(); // 操作失败(如类型转换失败)
stream.bad(); // 不可恢复错误
stream.clear(); // 重置所有状态格式化输出(iomanip)
std::cout << std::setw(15) << std::left << "Name"
<< std::setprecision(3) << 3.14159
<< std::fixed << 123.456
<< std::hex << 255 // 0xff
<< std::boolalpha << true; // "true"二进制读写
file.write(reinterpret_cast<const char*>(&obj), sizeof(obj));
file.read(reinterpret_cast<char*>(&obj), sizeof(obj));
// seekg(offset, beg/cur/end) — 移动读指针
// seekp(offset, beg/cur/end) — 移动写指针
// tellg() / tellp() — 获取当前指针位置字符串流
std::ostringstream oss;
oss << "Name: " << name << ", Age: " << age;
std::string result = oss.str();
std::istringstream iss("42 3.14 hello");
iss >> i >> d >> s; // 从字符串中解析C++17 filesystem
namespace fs = std::filesystem;
fs::exists(path); // 检查存在
fs::create_directory(dir); // 创建目录
fs::create_directories("a/b/c"); // 创建多级目录
for (auto& entry : fs::directory_iterator(dir)) { /* ... */ }
for (auto& entry : fs::recursive_directory_iterator(dir)) { /* ... */ }
auto size = fs::file_size(path);
fs::remove_all(dir); // 递归删除实践
力扣题目:力扣文件处理题(文件读入与字符串处理),力扣格式化输出题(格式化输出)。
AI 自检:1) 写出一个程序,读取 CSV 文件、解析每行数据并输出统计结果(要求处理文件不存在、格式错误的异常);2) 问 AI 在 Windows 和 Linux 上写入 "hello\n" 到二进制文件和文本文件的区别。
建议先阅读:10_异常处理 — 文件操作的异常安全处理;08_面向对象(四)运算符重载 — << 和 >> 是运算符重载最直观的应用。