文件流:ifstream、ofstream、fstream、文件读写模式、二进制文件操作

文件操作,说白了就是让程序和磁盘上的文件「对话」。我刚开始学C++时,总觉得文件读写不就是几个函数的事吗?直到我在项目中处理一个几十GB的日志文件,程序直接崩溃了……嗯,从那以后我再也不敢小看文件流了。

STL提供的文件流三剑客——ifstreamofstreamfstream,覆盖了读、写、读写三种场景。今天我们就把它彻底讲透。

1. 三剑客:谁负责什么?

先看一张图,理清它们的关系:

C++ 文件流三剑客 ios_base(基类) iostream(基类) ifstream(读) fstream(读写) ofstream(写) 只读模式 读写模式 只写模式

简单来说:

  • ifstream:只管读,不能写。适合读取配置文件、日志分析等场景。
  • ofstream:只管写,不能读。适合输出结果、保存数据。
  • fstream:既能读又能写。但要注意,它默认是读写模式,需要配合正确的打开方式。
我的习惯:能用 ifstream 或 ofstream 就别用 fstream。为什么?接口更清晰,不容易误操作。我在代码审查时见过有人用 fstream 只读打开文件,结果不小心写了脏数据进去……这种 bug 很难查。

2. 文件打开模式:别让模式坑了你

文件打开模式是文件流最容易被忽视的地方。我见过太多人因为模式用错,导致数据丢失或读取失败。

模式 含义 典型场景
ios::in 打开文件用于读取 ifstream 默认模式
ios::out 打开文件用于写入(会清空原有内容) ofstream 默认模式
ios::app 追加模式,写入位置在文件末尾 日志文件追加
ios::ate 打开后定位到文件末尾 需要知道文件大小时
ios::binary 以二进制模式打开 图片、音频、序列化数据
ios::trunc 如果文件存在,清空内容 配合 out 使用
我曾经踩过的坑:用 ofstream 写日志,忘了加 ios::app,结果每次运行程序都把之前的日志清空了。排查了半天,最后发现是模式问题。记住:追加日志一定要用 ios::app

3. 文本文件读写:最基础的操作

文本文件读写是日常开发中最常用的。来看一个完整的例子:

#include <iostream>
#include <fstream>
#include <string>

// 写入文本文件
void writeTextFile(const std::string& filename) {
    std::ofstream outFile(filename);
    if (!outFile.is_open()) {
        std::cerr << "文件打开失败!" << std::endl;
        return;
    }
    
    outFile << "姓名: 张三" << std::endl;
    outFile << "年龄: 28" << std::endl;
    outFile << "城市: 北京" << std::endl;
    
    outFile.close();  // 记得关闭
}

// 读取文本文件
void readTextFile(const std::string& filename) {
    std::ifstream inFile(filename);
    if (!inFile) {  // 也可以这样判断
        std::cerr << "文件不存在或无法打开!" << std::endl;
        return;
    }
    
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << std::endl;
    }
    
    inFile.close();
}

int main() {
    writeTextFile("info.txt");
    readTextFile("info.txt");
    return 0;
}

这里有几个细节要注意:

  • is_open()operator bool 都可以判断文件是否打开成功。我个人习惯用 if (!inFile),更简洁。
  • getline() 会读取一行,但不包含换行符。如果你需要保留换行符,可以用 get() 逐字符读取。
  • 文件用完一定要 close()。虽然析构函数会自动关闭,但显式关闭是良好的编程习惯。

4. 二进制文件操作:处理非文本数据

二进制文件操作是很多初学者的盲区。你想想看,图片、音频、视频、序列化数据,这些都不是纯文本,必须用二进制模式处理。

二进制读写的核心是两个函数:read()write()。它们的签名是:

// 从文件流读取 count 个字节到 buffer
istream& read(char* buffer, streamsize count);

// 将 buffer 中的 count 个字节写入文件流
ostream& write(const char* buffer, streamsize count);

来看一个实际例子:保存和读取结构体数据。

#include <iostream>
#include <fstream>
#include <cstring>

struct Student {
    char name[32];
    int age;
    double score;
};

// 写入二进制文件
void writeBinary(const std::string& filename) {
    std::ofstream outFile(filename, std::ios::binary);
    if (!outFile) {
        std::cerr << "打开文件失败" << std::endl;
        return;
    }
    
    Student stu;
    std::strcpy(stu.name, "李四");
    stu.age = 22;
    stu.score = 95.5;
    
    // 直接写入结构体内存
    outFile.write(reinterpret_cast<const char*>(&stu), sizeof(Student));
    outFile.close();
}

// 读取二进制文件
void readBinary(const std::string& filename) {
    std::ifstream inFile(filename, std::ios::binary);
    if (!inFile) {
        std::cerr << "打开文件失败" << std::endl;
        return;
    }
    
    Student stu;
    inFile.read(reinterpret_cast<char*>(&stu), sizeof(Student));
    
    std::cout << "姓名: " << stu.name << std::endl;
    std::cout << "年龄: " << stu.age << std::endl;
    std::cout << "成绩: " << stu.score << std::endl;
    
    inFile.close();
}

int main() {
    writeBinary("student.dat");
    readBinary("student.dat");
    return 0;
}
重要提醒:二进制文件在不同平台、不同编译器下可能不兼容。比如结构体对齐、字节序(大端/小端)都会影响数据。跨平台传输二进制数据时,建议使用序列化库(如 Protocol Buffers、Boost.Serialization)。

5. 文件流的状态与错误处理

文件操作不可能永远一帆风顺。磁盘满了、权限不够、文件被占用……这些情况都要处理。

文件流提供了几个状态标志:

std::ifstream file("test.txt");

// 检查文件流状态
if (file.good()) {
    // 一切正常
}
if (file.eof()) {
    // 到达文件末尾
}
if (file.fail()) {
    // 上次操作失败(比如类型不匹配)
}
if (file.bad()) {
    // 发生严重错误(比如磁盘损坏)
}

// 重置状态
file.clear();  // 清除所有错误标志

我在项目中处理过一个问题:读取一个 CSV 文件时,某一行数据格式不对,failbit 被置位,导致后续所有读取都失败。解决方案就是每次读取后检查状态,必要时 clear() 并跳过错误行。

6. 文件定位:随机访问

有时候我们需要跳到文件的某个位置读写,而不是从头开始。这就用到了文件定位:

std::ifstream file("data.bin", std::ios::binary);

// 获取当前读取位置
std::streampos pos = file.tellg();

// 跳转到第100个字节
file.seekg(100, std::ios::beg);

// 从当前位置向后跳20个字节
file.seekg(20, std::ios::cur);

// 从文件末尾向前跳50个字节
file.seekg(-50, std::ios::end);

// 对于 ofstream,使用 tellp() 和 seekp()
实用技巧:seekg(0, std::ios::end) 配合 tellg() 可以快速获取文件大小。这在处理二进制文件时非常有用。

7. 实战建议:文件操作的黄金法则

最后,分享几条我在实战中总结的经验:

  1. 总是检查文件是否打开成功。不要假设文件一定存在。
  2. 优先使用 RAII 方式。让文件流在作用域结束时自动关闭,避免忘记 close()。
  3. 文本文件用 getline() 逐行读取,二进制文件用 read()/write() 按块操作。
  4. 跨平台时注意路径分隔符。Windows 用反斜杠,Linux/macOS 用正斜杠。建议用正斜杠,或者用 std::filesystem::path
  5. 大文件不要一次性读入内存。用缓冲区逐块处理,否则内存会爆。

文件流看似简单,但用好了能解决很多实际问题。我当年做一个数据采集系统,每天要处理上百个日志文件,就是靠文件流配合多线程搞定的。记住:工具不在多,在于用得精。


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