搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第21章 文件输入输出

第21章 文件输入输出

进阶 👤 已掌握基础数据类型、类和STL容器的开发者 🔧 实现程序数据的持久化存储与交换
本章你将学到
  • 使用fstream进行文本和二进制文件的读写
  • 理解并应用文件打开模式与状态检测
  • 掌握序列化与反序列化的基本方法
核心概念文件流序列化

程序运行时的数据都存储在内存里,断电就消失。文件就像程序的“笔记本”,可以把计算结果、用户配置、游戏存档等数据永久保存到硬盘上。在C++中,我们通过fstream(文件流)这个工具来读写文件,它把文件抽象成一个可以顺序读写的字节流。

文件流类比

类比: 文件流就像连接水龙头和水池的水管,水龙头是程序,水池是硬盘上的文件,水就是数据。技术定义: fstream是C++标准库中用于文件操作的类,它继承自iostream,提供了与cin/cout类似的操作符(<<, >>)和方法,用于在程序和外部文件之间传输数据。

文件流家族与打开模式

首先,你需要包含头文件<fstream>。C++提供了三种主要的文件流类,对应不同的操作意图。

流类用途继承自
std::ifstream输入文件流,只用于从文件读取数据。std::istream
std::ofstream输出文件流,只用于向文件写入数据。std::ostream
std::fstream文件流,既可读又可写。std::iostream

创建流对象时,可以指定文件名和打开模式。打开模式决定了文件如何被处理,这是文件操作中第一个容易踩坑的地方。

关键点:文件打开模式

打开模式是std::ios类的静态常量,用位或操作符|组合使用。最常用的组合有:

  • std::ios::out | std::ios::trunc:写入,如果文件存在则清空内容(默认ofstream行为)。
  • std::ios::out | std::ios::app:写入,追加到文件末尾。
  • std::ios::in:读取(默认ifstream行为)。
  • std::ios::in | std::ios::out:读写,文件必须存在。
  • std::ios::binary:以二进制模式打开,处理非文本数据时必须加上。

我在项目中踩过这个坑:用std::ios::out模式打开一个不存在的文件进行写入,程序会创建它。但如果你用std::ios::in | std::ios::out模式打开一个不存在的文件,流会进入失败状态,后续所有操作都会无效。

文本文件读写实战

文本文件读写最简单,直接使用<<和>>操作符,就像用cout和cin一样。但要注意,>>操作符以空白字符(空格、换行、制表符)作为分隔。

// 示例:写入和读取一个简单的配置文件
#include <fstream>
#include <iostream>
#include <string>

int main() {
    // 1. 写入文本文件
    std::ofstream outFile("config.txt");
    if (!outFile.is_open()) {
        std::cerr << "无法打开文件用于写入!" << std::endl;
        return 1;
    }
    outFile << "PlayerName = " << "Alice" << "\n";
    outFile << "Level = " << 42 << "\n";
    outFile << "Resolution = " << 1920 << "x" << 1080 << std::endl;
    outFile.close(); // 显式关闭是好习惯

    // 2. 读取文本文件
    std::ifstream inFile("config.txt");
    std::string key, equalSign, value;
    while (inFile >> key >> equalSign >> value) {
        std::cout << "Key: " << key << ", Value: " << value << std::endl;
    }
    // 注意:这里“1920x1080”会被“>>”拆分成两个值
    return 0;
}

常见误区

  • 不检查文件是否成功打开: 直接进行读写操作,如果文件不存在或权限不足,程序会静默失败。务必使用is_open()或直接判断流状态(if (inFile))。
  • 混淆>>和getline: operator>>会跳过空白符,不适合读取包含空格的整行文本。读取一行应用std::getline(stream, string_var)。
  • 忘记关闭文件或依赖析构: 虽然流对象析构时会自动关闭文件,但在长时间运行或频繁打开文件的程序中,显式调用close()能及时释放系统资源,避免句柄泄漏。

二进制文件与序列化

当需要保存一个类的对象、一个结构体数组或任何非纯文本数据时,必须使用二进制模式。这个过程称为序列化(把内存对象变成字节流)和反序列化(从字节流重建对象)。

序列化类比

类比: 序列化就像把乐高模型拆成零件清单和拼装图打包进盒子;反序列化就是按照清单和图纸把零件重新拼成模型。技术定义: 序列化是将数据结构或对象状态转换为可以存储或传输的格式(通常是字节序列)的过程,反序列化是其逆过程。

二进制读写使用流对象的read()和write()成员函数。它们的参数是内存地址(char*)和字节数。

struct GameSave {
    char playerName[20];
    int level;
    float health;
    // 注意:结构体内不能有string、vector等动态容器!
};

// 二进制写入
GameSave save = {"Bob", 10, 85.5f};
std::ofstream binOut("save.dat", std::ios::binary);
binOut.write(reinterpret_cast<char*>(&save), sizeof(GameSave));

// 二进制读取
GameSave loadedSave;
std::ifstream binIn("save.dat", std::ios::binary);
binIn.read(reinterpret_cast<char*>(&loadedSave), sizeof(GameSave));

编者提示: 上面代码中使用的reinterpret_cast是一种危险的类型转换,它仅仅重新解释内存中的比特位。关于C++中四种类型转换操作符(static_cast, dynamic_cast, const_cast, reinterpret_cast)的安全用法和区别,我们将在第24章《类型转换操作符》中详细剖析。

实际开发中,大部分团队不会直接用二进制写整个结构体,因为存在字节序(Endianness)、内存对齐(Alignment)和指针失效三大问题。对于包含动态数据的类,需要设计自定义的序列化函数。

内存对象
→
序列化函数
→
二进制文件
↓
二进制文件
→
反序列化函数
→
内存对象

文件定位与随机访问

文件流内部维护一个“当前位置”指针。对于顺序读写,指针自动后移。但有时我们需要跳转到文件特定位置进行读写,这就是随机访问。

读取大文件中间部分

  • 跳过文件头部的元数据。
  • 直接读取中间某个数据块。
  • 避免加载整个文件到内存。

修改文件特定记录

  • 在数据库式文件中定位。
  • 读取-修改-写回固定长度记录。
  • 要求记录长度固定。

追加日志

  • 快速定位到文件末尾。
  • 写入新的日志条目。
  • 使用ios::app模式更简单。

控制文件位置指针使用seekg()(用于输入流定位读位置)和seekp()(用于输出流定位写位置)。获取当前位置使用tellg()和tellp()。

std::fstream file("data.bin", std::ios::in | std::ios::out | std::ios::binary);
// 将读指针移动到距文件开头第1024字节处
file.seekg(1024, std::ios::beg);
// 将写指针移动到文件末尾
file.seekp(0, std::ios::end);
long endPos = file.tellp(); // 获取文件总大小

seek的第二个参数是定位基准,有三个选项:std::ios::beg(文件开头),std::ios::cur(当前位置),std::ios::end(文件末尾)。如果你看到程序读取的数据全是乱码,大概率是文件指针定位错了地方,或者忘记以二进制模式打开文件。

动手试一试

创建一个简单的通讯录程序,功能如下:

  1. 定义Contact结构体,包含name(字符串)、phone(字符串)、id(整数)成员。
  2. 实现“添加联系人”功能:将一个新的Contact对象以文本格式追加写入到contacts.txt文件中,每行存储一个联系人,字段用逗号分隔(例如:张三,13800138000,1)。
  3. 实现“列出所有联系人”功能:读取contacts.txt文件,解析每一行,并将联系人信息显示在控制台。
  4. 挑战: 修改程序,将联系人数据以二进制格式存储到contacts.dat中。思考如何解决name和phone字符串长度不固定的问题?(提示:可以先写入字符串长度,再写入字符串内容)。

检验你的理解

  1. 以下代码意图是向log.txt追加一行日志,但它有错误。请指出错误并修正。
    std::ofstream logFile("log.txt");
    logFile << "Program started." << std::endl;
  2. 二进制模式std::ios::binary和文本模式的主要区别是什么?在Windows和Linux上,这个区别对换行符(‘\n’)的处理有何不同?
  3. 如果对一个std::ifstream对象调用write()方法,会发生什么?编译器会报错吗?为什么?

本章小结

  • 文件流是桥梁: ifstream, ofstream, fstream分别用于读、写和读写文件,需配合正确的打开模式使用。
  • 文本与二进制是两种世界: 文本文件使用<<和>>操作符,便于人类阅读;二进制文件使用read()/write(),用于保存原始内存数据,必须处理序列化问题。
  • 状态检查是关键: 任何文件操作前后都应检查流状态(is_open(), !stream.fail()),这是写出健壮程序的基石。
  • 指针控制实现随机访问: 通过seekg()/seekp()和tellg()/tellp()可以在文件中任意位置进行读写,适用于大数据文件或数据库类应用。
  • 序列化是核心技能: 将复杂对象(尤其是包含动态内存的对象)持久化到文件,需要设计专门的序列化与反序列化逻辑,不能简单内存拷贝。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注