文件与流

原理

流的缓冲区三层架构

C++ IO 流采用三层架构,将用户程序的格式化和底层系统调用解耦:

用户程序层:  ofstream / ifstream / cout    ← 格式化 >> / << / getline
缓冲区层:    streambuf / filebuf           ← 内部字符数组,减少系统调用
系统调用层:  read() / write() (POSIX)      ← 用户态→内核态切换

输出流程

ofstream << "hello" 的数据流经路径:

  1. 数据写入 streambuf 的内部缓冲区
  2. 缓冲区满(或显式 flush、遇到 endl、程序正常结束)时
  3. 通过 write() 系统调用一次性写入内核
  4. 内核将数据写入磁盘(可能通过页缓存进一步延迟)

读取流程

ifstream >> value 的数据流经路径:

  1. 内核通过 read() 从磁盘读取一大块数据(如 4KB)
  2. 放入 streambuf 的缓冲区
  3. 用户程序从缓冲区逐字节/逐行取数据
  4. 缓冲区耗尽时再次触发 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)),允许混合使用 coutprintf。同步的代价是每次 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_面向对象(四)运算符重载<<>> 是运算符重载最直观的应用。