第29章:文件与流——fstream、文件模式与stringstream

文件操作,说白了就是让程序跟硬盘上的文件打交道。我刚开始学C++那会儿,总觉得文件读写是个「高级功能」,后来才发现——这玩意儿几乎每个项目都会用到。配置文件的读取、日志的输出、数据的持久化……哪样都离不开它。

这一章,咱们就彻底搞懂C++的文件流。包括ifstream、ofstream、fstream怎么用,文本文件和二进制文件的区别,还有那个特别好用的stringstream。

29.1 文件流的三兄弟:ifstream、ofstream、fstream

C++标准库提供了三个文件流类,都在头文件里:

  • ifstream:输入文件流,用来从文件读数据
  • ofstream:输出文件流,用来往文件写数据
  • fstream:文件流,既能读也能写

它们都继承自istream/ostream,所以用法跟cin/cout很像。我个人习惯是:如果只需要读,就用ifstream;只需要写,就用ofstream;需要同时读写,才用fstream。这样代码意图更清晰。

核心要点:文件流对象在生命周期结束时,会自动调用close()关闭文件。但显式调用close()是个好习惯——尤其是你要在同一个对象上打开另一个文件时。

29.2 文件打开模式

打开文件时,你可以指定打开模式。这些模式定义在std::ios_base里,用位掩码表示:

模式常量 含义 说明
ios::in 读模式 打开文件用于读取
ios::out 写模式 打开文件用于写入(默认会清空文件)
ios::app 追加模式 每次写入都追加到文件末尾
ios::ate 定位到末尾 打开后文件指针在末尾,但可以移动
ios::trunc 截断模式 打开文件时清空已有内容
ios::binary 二进制模式 以二进制方式读写,不进行文本转换

多个模式可以用|组合。比如:

std::ofstream file("data.txt", std::ios::out | std::ios::app);

这表示以追加模式打开文件。如果文件不存在,会创建它。

注意:ofstream默认就带了ios::out模式。如果你只写ofstream file("test.txt"),它等价于ios::out | ios::trunc——也就是说,文件原有内容会被清空。我曾经因为这个坑,把客户的重要配置文件给覆盖了……从那以后,我每次打开文件都会显式指定模式。

29.3 文本文件读写

文本文件读写是最常见的操作。说白了,就是把数据以人类可读的文本形式存起来。

写入文本文件

#include <fstream>
#include <iostream>

int main() {
    std::ofstream outFile("example.txt");
    if (!outFile.is_open()) {
        std::cerr << "文件打开失败!" << std::endl;
        return 1;
    }

    outFile << "姓名:张三" << std::endl;
    outFile << "年龄:25" << std::endl;
    outFile << "分数:92.5" << std::endl;

    outFile.close();
    return 0;
}

你看,跟cout用法一模一样。用<<运算符就行。

读取文本文件

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

int main() {
    std::ifstream inFile("example.txt");
    if (!inFile) {  // 也可以这样判断
        std::cerr << "文件打开失败!" << std::endl;
        return 1;
    }

    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << std::endl;
    }

    inFile.close();
    return 0;
}

这里用getline逐行读取,是最稳妥的方式。为什么?因为用>>读取字符串时,遇到空格就停了。而getline会读取一整行,包括空格。

小技巧:判断文件是否打开成功,可以用if (!file)或者if (!file.is_open())。我个人更推荐前者,因为ifstream重载了bool运算符,更简洁。

29.4 二进制文件读写

二进制文件读写,跟文本文件最大的区别是:没有格式转换。数据在内存里是什么样子,写到文件里就是什么样子。适合存结构体、数组等原始数据。

我做过一个项目,需要把传感器采集的百万级数据点存下来。如果用文本文件,每个浮点数转成字符串再写,不仅慢,文件还大。用二进制读写,速度直接快了10倍。

写入二进制文件

#include <fstream>
#include <iostream>

struct SensorData {
    int id;
    double value;
    char status;
};

int main() {
    SensorData data = {1, 23.456, 'A'};

    std::ofstream outFile("sensor.bin", std::ios::binary);
    if (!outFile) {
        std::cerr << "文件打开失败!" << std::endl;
        return 1;
    }

    outFile.write(reinterpret_cast<const char*>(&data), sizeof(data));
    outFile.close();
    return 0;
}

读取二进制文件

#include <fstream>
#include <iostream>

struct SensorData {
    int id;
    double value;
    char status;
};

int main() {
    SensorData data;

    std::ifstream inFile("sensor.bin", std::ios::binary);
    if (!inFile) {
        std::cerr << "文件打开失败!" << std::endl;
        return 1;
    }

    inFile.read(reinterpret_cast<char*>(&data), sizeof(data));
    std::cout << "ID: " << data.id << ", 值: " << data.value
              << ", 状态: " << data.status << std::endl;

    inFile.close();
    return 0;
}

警告:二进制文件在不同平台、不同编译器下可能不兼容。比如结构体对齐、字节序(大端/小端)都会影响数据的布局。如果你要在不同系统间交换数据,最好用文本格式或者定义明确的序列化协议。

29.5 stringstream:内存中的流

stringstream是个好东西。它让你像操作文件流一样操作字符串。说白了,就是把字符串当成一个「虚拟文件」来读写。

它有三个变种:

  • istringstream:从字符串读
  • ostringstream:往字符串写
  • stringstream:既能读也能写

典型用法:字符串拼接

#include <sstream>
#include <iostream>
#include <string>

int main() {
    std::ostringstream oss;
    oss << "用户ID: " << 1001 << ", 分数: " << 95.5;
    std::string result = oss.str();
    std::cout << result << std::endl;
    return 0;
}

比用+拼接字符串优雅多了,而且性能更好。为什么?因为+会创建临时对象,而ostringstream内部有缓冲区。

典型用法:字符串解析

#include <sstream>
#include <iostream>
#include <string>

int main() {
    std::string data = "1001 张三 92.5";
    std::istringstream iss(data);

    int id;
    std::string name;
    double score;

    iss >> id >> name >> score;
    std::cout << "ID: " << id << ", 姓名: " << name
              << ", 分数: " << score << std::endl;
    return 0;
}

你看,从字符串里提取数据,就像从文件里读一样简单。

实用技巧:stringstream经常用来做类型转换。比如把int转成string,或者把string转成double。比atoi、itoa安全多了。

29.6 本章知识体系

下面这张图,帮你理清文件与流的核心脉络:

文件与流核心知识体系 C++ 文件流 ifstream(读) ofstream(写) fstream(读写) 文件打开模式:in / out / app / ate / trunc / binary 文本文件:<< / getline / >> 二进制文件:write / read stringstream:内存中的流(istringstream / ostringstream)

29.7 避坑指南

最后,分享几个我踩过的坑:

  • 忘记检查文件是否打开成功——我曾经写了个日志模块,结果磁盘满了,文件没打开,程序还在那疯狂写……最后日志全丢了。从那以后,每次打开文件我都检查is_open()。
  • 二进制文件用文本模式打开——在Windows上,文本模式会把\n转成\r\n,二进制数据就全乱了。记住:二进制文件一定要用ios::binary。
  • stringstream忘记清空——如果你重复使用同一个stringstream对象,记得调用str("")和clear()。不然上次的内容还在里面。
  • 文件路径用反斜杠——在Windows上,路径用\需要转义,比如"c:\\data\\file.txt"。我建议统一用正斜杠"/",跨平台兼容。

嗯,文件与流这部分,说白了就是「打开-读写-关闭」三步走。但细节决定成败。把上面这些点记住了,你写文件操作代码的时候,就能少踩很多坑。


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