11. 文件输入输出:文件流与文件操作

文件操作,说白了就是让程序能读写硬盘上的数据。我刚开始学C++时,觉得这功能没什么大不了。直到有一次,我写了一个学生成绩管理系统,每次关闭程序数据就丢了……嗯,从那以后,我才真正重视起文件操作。

这一章,我们来聊聊C++里怎么跟文件打交道。我会把文件流、文本文件、二进制文件这些概念,用我自己的经验串起来讲。

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

C++把文件看作一种流。就像cin和cout是标准输入输出流一样,文件也有自己的流对象。它们都定义在头文件里。

这三兄弟的分工很明确:

  • ifstream:专门用来读文件(input file stream)
  • ofstream:专门用来写文件(output file stream)
  • fstream:既能读又能写,是前两个的合体
我的习惯:如果只需要读或写,我尽量用ifstream或ofstream,而不是fstream。这样代码意图更清晰,别人一看就知道你要干嘛。

打开文件的方式很简单:

#include <fstream>
#include <iostream>
using namespace std;

int main() {
    // 方式一:构造时直接打开
    ofstream outFile("data.txt");
    
    // 方式二:先构造,再手动打开
    ifstream inFile;
    inFile.open("data.txt");
    
    // 方式三:用fstream,指定模式
    fstream ioFile;
    ioFile.open("data.txt", ios::in | ios::out);
    
    return 0;
}

打开模式有很多种,我整理了一张表:

模式常量 含义 说明
ios::in 读模式 打开文件用于读取
ios::out 写模式 打开文件用于写入(会清空原有内容)
ios::app 追加模式 写入时追加到文件末尾,不清空
ios::binary 二进制模式 以二进制方式读写
ios::ate 初始位置在末尾 打开后文件指针在末尾,但可以移动
ios::trunc 截断文件 如果文件存在,清空内容
注意:ofstream默认就是ios::out模式,会清空文件。如果你不想清空,记得加上ios::app。我曾经因为这个坑,把同事的配置文件给清空了……场面一度很尴尬。

11.2 文本文件读写

文本文件,就是你能用记事本打开、看到文字的那种。读写方式跟cin/cout几乎一样。

写文本文件:

#include <fstream>
using namespace std;

int main() {
    ofstream fout("students.txt");
    
    if (!fout) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    
    fout << "张三" << " " << 85 << endl;
    fout << "李四" << " " << 92 << endl;
    fout << "王五" << " " << 78 << endl;
    
    fout.close();
    return 0;
}

读文本文件:

#include <fstream>
#include <iostream>
#include <string>
using namespace std;

int main() {
    ifstream fin("students.txt");
    
    if (!fin) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    
    string name;
    int score;
    
    while (fin >> name >> score) {
        cout << name << "的成绩是:" << score << endl;
    }
    
    fin.close();
    return 0;
}
关键点:文本文件读写用 << 和 >> 操作符,跟控制台一模一样。但要注意,>> 会跳过空白字符(空格、换行、制表符)。如果你要读取整行,请用 getline()。

读取整行的写法:

string line;
while (getline(fin, line)) {
    cout << line << endl;
}

11.3 二进制文件读写

二进制文件,存的是数据的原始字节。比如一个int是4个字节,二进制文件就直接存这4个字节,而不是存成"42"这样的字符串。

为什么要用二进制?我举个例子:你要存一个double数组,100万个元素。用文本文件存,每个数字可能占10-20个字符,文件很大。用二进制存,每个double固定8个字节,文件小,读写也快。

写二进制文件:

#include <fstream>
using namespace std;

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

int main() {
    Student stu = {"张三", 20, 88.5};
    
    ofstream fout("student.dat", ios::binary);
    if (!fout) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    
    // 用 write 函数写入二进制数据
    fout.write(reinterpret_cast<const char*>(&stu), sizeof(stu));
    
    fout.close();
    return 0;
}

读二进制文件:

#include <fstream>
#include <iostream>
using namespace std;

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

int main() {
    Student stu;
    
    ifstream fin("student.dat", ios::binary);
    if (!fin) {
        cerr << "文件打开失败!" << endl;
        return 1;
    }
    
    // 用 read 函数读取二进制数据
    fin.read(reinterpret_cast<char*>(&stu), sizeof(stu));
    
    cout << "姓名:" << stu.name << endl;
    cout << "年龄:" << stu.age << endl;
    cout << "成绩:" << stu.score << endl;
    
    fin.close();
    return 0;
}
注意:write和read的第一个参数都是char*类型。如果你要写入其他类型的数据,必须用reinterpret_cast强制转换。另外,结构体里尽量不要用string,因为string内部有指针,存到文件里再读回来,指针就失效了。用固定大小的char数组更安全。

11.4 文件状态检查

文件操作经常会出错。比如文件不存在、磁盘满了、权限不够。我建议每次打开文件后,都检查一下状态。

常用的检查方法:

ifstream fin("data.txt");

// 方法一:直接判断对象
if (!fin) {
    cerr << "文件打开失败" << endl;
}

// 方法二:用 is_open()
if (!fin.is_open()) {
    cerr << "文件未成功打开" << endl;
}

// 方法三:检查流状态
if (fin.fail()) {
    cerr << "操作失败" << endl;
}
if (fin.eof()) {
    cout << "已读到文件末尾" << endl;
}
if (fin.bad()) {
    cerr << "发生严重错误" << endl;
}

流状态位有四个:

状态位 含义 触发条件
goodbit 一切正常 没有错误
eofbit 到达文件末尾 读取到文件结束
failbit 操作失败 类型不匹配、文件不存在等
badbit 严重错误 底层流损坏
我的经验:我一般用 if (!fin) 做快速检查。如果要做更精细的错误处理,再分别检查 fail() 和 eof()。记住,读取完文件后,一定要先检查 eof() 再处理数据,否则可能读到无效数据。

11.5 随机文件访问

文件就像一条磁带,默认是顺序读写的。但有时候,你想跳到文件的某个位置去读写。比如一个文件存了1000条记录,你想直接读第500条,不需要从头读到第499条。

这就是随机访问。C++提供了两个关键函数:

  • seekg():设置读指针的位置(get pointer)
  • seekp():设置写指针的位置(put pointer)
  • tellg():获取当前读指针的位置
  • tellp():获取当前写指针的位置
#include <fstream>
#include <iostream>
using namespace std;

int main() {
    fstream file("data.bin", ios::in | ios::out | ios::binary);
    
    if (!file) {
        cerr << "文件打开失败" << endl;
        return 1;
    }
    
    // 跳到第5个位置(从0开始计数)
    file.seekg(5, ios::beg);
    
    char ch;
    file.get(ch);
    cout << "第5个字符是:" << ch << endl;
    
    // 获取当前位置
    streampos pos = file.tellg();
    cout << "当前位置:" << pos << endl;
    
    // 从当前位置往后跳3个字节
    file.seekg(3, ios::cur);
    
    // 从文件末尾往前跳2个字节
    file.seekg(-2, ios::end);
    
    file.close();
    return 0;
}

seekg的第二个参数有三个选项:

偏移基准 含义
ios::beg 从文件开头算起
ios::cur 从当前位置算起
ios::end 从文件末尾算起
实用场景:假设你有一个文件,存了100个学生的数据,每个学生占64字节。你想读第50个学生,直接 file.seekg(49 * 64, ios::beg) 就行了。这比循环49次快得多。

11.6 知识体系总览

下面这张图,把本章的核心知识点串在了一起。你可以把它当作一个快速索引:

文件输入输出知识体系 文件流 ifstream(读) ofstream(写) fstream(读写) 文本文件(<< >>) 二进制文件(read/write) 随机访问(seekg/seekp) 文件状态检查(good/fail/eof/bad) 打开文件 → 检查状态 → 读写操作 → 关闭文件

这张图展示了文件操作的核心流程。你从文件流出发,根据需求选择ifstream、ofstream或fstream。然后决定用文本方式还是二进制方式。如果需要跳转,就用随机访问。最后,别忘了检查文件状态,确保每一步都走对了。

文件操作其实不难,但细节很多。我建议你多写几个小例子练练手。比如写一个程序,把自己的名字和年龄存到文件里,再读出来。等你能熟练操作了,再尝试更复杂的场景。


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