C++流类库 文件流操作

我们进入下一个核心主题:文件流操作。这是C++流类库中实用性最强的部分之一,用于持久化存储和读取数据。


第四部分:文件流操作

C++通过 ifstreamofstreamfstream 三个类来处理文件。它们分别继承自 istreamostreamiostream,因此你之前学到的所有格式化控制、成员函数等都能直接用于文件流。

5.1 文件流类简介

cpp

复制代码
#include <fstream> // 必须包含此头文件

std::ifstream;  // 只用于输入(读文件)
std::ofstream;  // 只用于输出(写文件)
std::fstream;   // 既可用于输入也可用于输出(读写文件)
5.2 文件打开模式

打开文件时需要指定模式,这些模式在 ios_base 类中定义为常量,可以用位或操作符 | 组合使用。

模式常量 含义 适用于
std::ios::in 为输入(读)而打开 ifstream, fstream
std::ios::out 为输出(写)而打开 ofstream, fstream
std::ios::binary 以二进制模式打开 所有文件流
std::ios::ate 打开后定位到文件末尾 所有文件流
std::ios::app 所有输出都追加到文件末尾 ofstream, fstream
std::ios::trunc 如果文件存在,先清空它 ofstream, fstream(当out指定时默认)

重要说明:

  • 默认模式:

    • ifstream 默认 ios::in

    • ofstream 默认 ios::out | ios::trunc(输出并清空)

    • fstream 默认 ios::in | ios::out

  • ios::appios::ate 的区别:

    • app(append):所有写入都强制追加到文件尾,即使移动了文件指针。

    • ate(at end):打开时初始位置在文件尾,但之后可以移动指针到其他地方写。

  • ios::trunc:如果与 ios::in 一起使用,文件会被清空(可能不是你想要的)。

5.3 打开文件的几种方式

方式1:先创建对象,再调用 open()

cpp

复制代码
std::ofstream outFile;
outFile.open("example.txt", std::ios::out);
if (!outFile) { // 或者 outFile.fail(), !outFile.is_open()
    std::cerr << "无法打开文件 example.txt" << std::endl;
    return 1;
}
// 使用 outFile...
outFile.close();

方式2:在构造函数中直接打开(更简洁)

cpp

复制代码
std::ofstream outFile("example.txt", std::ios::out);
if (!outFile.is_open()) { // 显式检查
    std::cerr << "无法打开文件 example.txt" << std::endl;
    return 1;
}
// 使用 outFile...
// 文件会在 outFile 析构时自动关闭,但显式关闭是好习惯
outFile.close();

方式3:使用 fstream 进行读写

cpp

复制代码
std::fstream ioFile("data.txt", std::ios::in | std::ios::out);
if (!ioFile) {
    // 如果文件不存在,可能需要创建
    ioFile.open("data.txt", std::ios::out); // 创建文件
    ioFile.close();
    ioFile.open("data.txt", std::ios::in | std::ios::out);
}
5.4 文本文件与二进制文件
  • 文本文件 :存储的是字符序列,人类可读。使用 <<>> 运算符进行格式化IO。

  • 二进制文件 :存储的是字节序列,不可直接阅读。使用 read()write() 成员函数进行原始字节IO。

5.5 文本文件操作示例
示例1:写入文本文件

cpp

复制代码
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ofstream outFile("output.txt");
    if (!outFile) {
        std::cerr << "创建文件失败!" << std::endl;
        return 1;
    }
    
    // 像使用 cout 一样使用 outFile
    outFile << "Hello, File IO!" << std::endl;
    outFile << "整数: " << 42 << std::endl;
    outFile << "浮点数: " << 3.14159 << std::endl;
    
    std::string name = "Alice";
    int age = 25;
    outFile << "姓名: " << name << ", 年龄: " << age << std::endl;
    
    outFile.close();
    std::cout << "数据已写入 output.txt" << std::endl;
    
    return 0;
}
示例2:读取文本文件(多种方式)

cpp

复制代码
#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream inFile("output.txt");
    if (!inFile) {
        std::cerr << "无法打开文件!" << std::endl;
        return 1;
    }
    
    std::cout << "=== 方式1:逐词读取(使用 >>)===" << std::endl;
    std::string word;
    inFile.clear(); // 重置状态
    inFile.seekg(0); // 移动文件指针到开头
    
    while (inFile >> word) { // >> 会跳过空白字符
        std::cout << word << " ";
    }
    std::cout << std::endl;
    
    std::cout << "\n=== 方式2:逐行读取(推荐)===" << std::endl;
    inFile.clear();
    inFile.seekg(0);
    
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << std::endl;
    }
    
    std::cout << "\n=== 方式3:逐个字符读取 ===" << std::endl;
    inFile.clear();
    inFile.seekg(0);
    
    char ch;
    while (inFile.get(ch)) {
        std::cout << ch;
    }
    
    inFile.close();
    return 0;
}
示例3:文件拷贝(文本模式)

cpp

复制代码
#include <iostream>
#include <fstream>

bool copyTextFile(const std::string& source, const std::string& destination) {
    std::ifstream src(source, std::ios::in);
    if (!src) {
        std::cerr << "无法打开源文件: " << source << std::endl;
        return false;
    }
    
    std::ofstream dst(destination, std::ios::out);
    if (!dst) {
        std::cerr << "无法创建目标文件: " << destination << std::endl;
        src.close();
        return false;
    }
    
    // 关键:取消跳过空白字符,否则会丢失空格、换行等
    src.unsetf(std::ios::skipws);
    
    char ch;
    while (src >> ch) { // 现在会读取所有字符,包括空白
        dst << ch;
    }
    
    src.close();
    dst.close();
    
    std::cout << "文件拷贝完成: " << source << " -> " << destination << std::endl;
    return true;
}

int main() {
    copyTextFile("output.txt", "copy.txt");
    return 0;
}
5.6 二进制文件操作

二进制文件使用 read()write() 函数,它们直接操作内存字节。

write()read() 函数原型

cpp

复制代码
// 写入二进制数据
ostream& write(const char* buffer, streamsize size);

// 读取二进制数据
istream& read(char* buffer, streamsize size);

重要提示:

  • buffer 应该是内存块的起始地址

  • size 是要读写的字节数

  • 通常需要使用 reinterpret_cast<char*>() 来转换非字符类型指针

示例:二进制文件读写

cpp

复制代码
#include <iostream>
#include <fstream>
#include <cstring>

struct Person {
    char name[50];
    int age;
    double salary;
};

int main() {
    // 准备数据
    Person people[3] = {
        {"Alice", 25, 50000.0},
        {"Bob", 30, 60000.0},
        {"Charlie", 35, 70000.0}
    };
    
    // 1. 写入二进制文件
    std::ofstream outFile("people.dat", std::ios::binary);
    if (!outFile) {
        std::cerr << "无法创建二进制文件!" << std::endl;
        return 1;
    }
    
    for (int i = 0; i < 3; ++i) {
        outFile.write(reinterpret_cast<const char*>(&people[i]), sizeof(Person));
    }
    outFile.close();
    
    // 2. 读取二进制文件
    std::ifstream inFile("people.dat", std::ios::binary);
    if (!inFile) {
        std::cerr << "无法打开二进制文件!" << std::endl;
        return 1;
    }
    
    Person temp;
    std::cout << "从二进制文件读取的数据:" << std::endl;
    
    // 方法1:知道确切记录数
    for (int i = 0; i < 3; ++i) {
        inFile.read(reinterpret_cast<char*>(&temp), sizeof(Person));
        if (inFile.gcount() == sizeof(Person)) { // 确保读取完整
            std::cout << "姓名: " << temp.name 
                      << ", 年龄: " << temp.age 
                      << ", 薪资: " << temp.salary << std::endl;
        }
    }
    
    // 方法2:一直读到文件末尾
    inFile.clear();
    inFile.seekg(0); // 回到文件开头
    
    std::cout << "\n再次读取(使用eof检测):" << std::endl;
    while (!inFile.eof()) {
        inFile.read(reinterpret_cast<char*>(&temp), sizeof(Person));
        if (inFile.gcount() > 0) { // 实际读取到了数据
            std::cout << "姓名: " << temp.name 
                      << ", 年龄: " << temp.age 
                      << ", 薪资: " << temp.salary << std::endl;
        }
    }
    
    inFile.close();
    return 0;
}
二进制文件操作的注意事项
  1. 字节对齐(Padding) :结构体可能有内存对齐填充,sizeof(Person) 可能大于各成员大小之和。

  2. 平台依赖性:二进制数据与平台相关(如字节序、类型大小)。在不同平台间交换二进制文件可能有问题。

  3. 指针和动态内存:不能直接读写包含指针的结构体,因为指针值在其他程序运行中无意义。需要序列化(serialize)数据。

  4. 字符串处理 :使用固定大小的字符数组(如 char name[50])而不是 std::string,因为 std::string 包含指针。

5.7 文件随机访问

通过移动文件指针,可以在文件中任意位置读写,特别是对二进制文件。

文件定位函数

cpp

复制代码
// 获取当前读取位置
streampos tellg();

// 设置读取位置
istream& seekg(streampos pos);
istream& seekg(streamoff off, ios_base::seekdir dir);

// 获取当前写入位置
streampos tellp();

// 设置写入位置
ostream& seekp(streampos pos);
ostream& seekp(streamoff off, ios_base::seekdir dir);

seekdir 枚举:

  • ios::beg:从文件开头开始

  • ios::cur:从当前位置开始

  • ios::end:从文件末尾开始

示例:随机访问二进制文件

cpp

复制代码
#include <iostream>
#include <fstream>

struct Record {
    int id;
    char data[100];
};

int main() {
    // 创建一些记录
    Record records[5];
    for (int i = 0; i < 5; ++i) {
        records[i].id = i + 1;
        sprintf(records[i].data, "这是第%d条记录", i + 1);
    }
    
    // 写入文件
    std::fstream file("records.dat", 
                      std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc);
    
    for (int i = 0; i < 5; ++i) {
        file.write(reinterpret_cast<const char*>(&records[i]), sizeof(Record));
    }
    
    // 随机读取:直接读取第3条记录(索引2)
    std::cout << "读取第3条记录:" << std::endl;
    Record rec;
    
    // 计算位置:每条记录大小 * 索引
    file.seekg(2 * sizeof(Record), std::ios::beg);
    file.read(reinterpret_cast<char*>(&rec), sizeof(Record));
    
    std::cout << "ID: " << rec.id << ", 数据: " << rec.data << std::endl;
    
    // 修改第2条记录(索引1)
    std::cout << "\n修改第2条记录:" << std::endl;
    rec.id = 999;
    strcpy(rec.data, "这条记录被修改了!");
    
    file.seekp(1 * sizeof(Record), std::ios::beg);
    file.write(reinterpret_cast<const char*>(&rec), sizeof(Record));
    
    // 验证修改
    file.seekg(1 * sizeof(Record), std::ios::beg);
    file.read(reinterpret_cast<char*>(&rec), sizeof(Record));
    std::cout << "修改后 - ID: " << rec.id << ", 数据: " << rec.data << std::endl;
    
    // 从文件末尾添加新记录
    std::cout << "\n在文件末尾添加新记录:" << std::endl;
    rec.id = 1000;
    strcpy(rec.data, "这是新添加的记录");
    
    file.seekp(0, std::ios::end); // 移动到文件末尾
    file.write(reinterpret_cast<const char*>(&rec), sizeof(Record));
    
    // 获取文件中的记录总数
    file.seekg(0, std::ios::end);
    streampos fileSize = file.tellg();
    int numRecords = fileSize / sizeof(Record);
    std::cout << "文件中共有 " << numRecords << " 条记录" << std::endl;
    
    file.close();
    return 0;
}
5.8 文件操作的完整模式

在面向对象程序中,文件操作通常与对象的生命周期绑定:

cpp

复制代码
#include <iostream>
#include <fstream>
#include <vector>

class Student {
private:
    int id;
    std::string name;
    double score;
    
public:
    Student(int i = 0, const std::string& n = "", double s = 0.0) 
        : id(i), name(n), score(s) {}
    
    // 显示
    void display(std::ostream& os = std::cout) const {
        os << "学号: " << id << ", 姓名: " << name << ", 分数: " << score;
    }
    
    // 重载输出运算符(用于文本文件)
    friend std::ostream& operator<<(std::ostream& os, const Student& s) {
        os << s.id << " " << s.name << " " << s.score;
        return os;
    }
    
    // 重载输入运算符(用于文本文件)
    friend std::istream& operator>>(std::istream& is, Student& s) {
        is >> s.id >> s.name >> s.score;
        return is;
    }
    
    // 二进制保存
    void saveBinary(std::ofstream& ofs) const {
        // 保存id
        ofs.write(reinterpret_cast<const char*>(&id), sizeof(id));
        
        // 保存字符串:先保存长度,再保存内容
        size_t len = name.length();
        ofs.write(reinterpret_cast<const char*>(&len), sizeof(len));
        ofs.write(name.c_str(), len);
        
        // 保存分数
        ofs.write(reinterpret_cast<const char*>(&score), sizeof(score));
    }
    
    // 二进制加载
    void loadBinary(std::ifstream& ifs) {
        // 读取id
        ifs.read(reinterpret_cast<char*>(&id), sizeof(id));
        
        // 读取字符串
        size_t len;
        ifs.read(reinterpret_cast<char*>(&len), sizeof(len));
        char* buffer = new char[len + 1];
        ifs.read(buffer, len);
        buffer[len] = '\0';
        name = buffer;
        delete[] buffer;
        
        // 读取分数
        ifs.read(reinterpret_cast<char*>(&score), sizeof(score));
    }
};

class StudentManager {
private:
    std::vector<Student> students;
    std::string filename;
    
public:
    StudentManager(const std::string& fname) : filename(fname) {}
    
    // 从文件加载学生(构造函数中)
    void loadFromFile() {
        std::ifstream inFile(filename);
        if (!inFile) {
            std::cout << "文件不存在,将创建新文件。" << std::endl;
            return;
        }
        
        Student s;
        while (inFile >> s) {
            students.push_back(s);
        }
        inFile.close();
        std::cout << "从文件加载了 " << students.size() << " 名学生" << std::endl;
    }
    
    // 保存学生到文件(析构函数中)
    void saveToFile() {
        std::ofstream outFile(filename);
        if (!outFile) {
            std::cerr << "无法保存到文件!" << std::endl;
            return;
        }
        
        for (const auto& s : students) {
            outFile << s << std::endl;
        }
        outFile.close();
        std::cout << "已保存 " << students.size() << " 名学生到文件" << std::endl;
    }
    
    // 其他管理函数...
    void addStudent(const Student& s) {
        students.push_back(s);
    }
    
    void displayAll() const {
        for (const auto& s : students) {
            s.display();
            std::cout << std::endl;
        }
    }
};

int main() {
    StudentManager manager("students.txt");
    
    // 加载已有数据
    manager.loadFromFile();
    
    // 添加新学生
    manager.addStudent(Student(1001, "张三", 85.5));
    manager.addStudent(Student(1002, "李四", 92.0));
    manager.addStudent(Student(1003, "王五", 78.5));
    
    // 显示所有学生
    std::cout << "\n所有学生信息:" << std::endl;
    manager.displayAll();
    
    // 保存到文件(在实际应用中,这可能在析构函数中自动调用)
    manager.saveToFile();
    
    return 0;
}

这部分的核心总结:

  1. 三个文件流类ifstream(读)、ofstream(写)、fstream(读写)。

  2. 文件打开模式 :理解 inoutappatetruncbinary 的含义和组合。

  3. 文本文件操作 :像使用 cin/cout 一样使用文件流对象,注意 unsetf(ios::skipws) 在完整拷贝时的重要性。

  4. 二进制文件操作 :使用 read()/write() 函数,注意类型转换和平台依赖性。

  5. 随机访问 :通过 seekg()/seekp()tellg()/tellp() 在文件中定位,特别适用于数据库风格的记录存取。

  6. 与对象结合:在面向对象设计中,通常在构造函数中加载数据,在析构函数中保存数据。

相关推荐
缘三水1 小时前
【C语言】10.操作符详解(下)
c语言·开发语言·c++·语法·基础定义
渡我白衣1 小时前
深入理解算法库的灵魂——彻底掌握 <algorithm> 的范式、迭代器约束、隐藏陷阱与性能真相
数据结构·c++·人工智能·网络协议·mysql·rpc·dubbo
Gomiko1 小时前
JavaScript基础(九):内部对象
开发语言·javascript·udp
smile_Iris1 小时前
Day 26 常见的降维算法
开发语言·算法·kotlin
暗然而日章1 小时前
C++基础:Stanford CS106L学习笔记 3 流
c++·笔记·学习
刻刻帝的海角1 小时前
响应式数据可视化 Dashboard
开发语言·前端·javascript
王铁柱子哟-1 小时前
如何在 VS Code 中调试带参数和环境变量的 Python 程序
开发语言·python
獭.獭.1 小时前
C++ -- STL【list的使用】
c++·stl·list
Q741_1471 小时前
C++ 栈 模拟 1047. 删除字符串中的所有相邻重复项 题解 每日一题
c++·算法·leetcode·模拟·