C++ 文件读写(fstream)

平时我们写练习代码时,程序运行完把结果打印到屏幕上,内存里的数据也就跟着清空了。但在实际开发中,软件配置、运行日志和游戏存档这类数据都需要保存到磁盘文件中。只有存进文件,下次启动程序时我们才能把数据重新读出来。

在 C++ 中,程序与磁盘文件交互主要依靠 “文件流”。完整操作一个文件,通常按顺序包含以下 3 个步骤:

  • 打开文件:创建文件流对象,让程序与磁盘上的指定文件建立连接。
  • 读写数据:通过文件流向文件中写入内容,或者从文件中读取内容。
  • 关闭文件:读写结束后断开连接,释放占用的系统资源。

C++ 文件流是什么?

初学的小伙伴看到 “流(stream)” 这个词,可能会觉得有点抽象。其实你可以把它想象成程序与外部文件之间的一条 “数据管道”,数据就像水流一样顺着这条管道进出,如下图所示。

用数据管道理解 C++ 文件流的双向读写机制

这和我们熟悉的控制台输入输出非常相似。其中,std::cin 负责让数据从键盘流进程序,而 std::cout 负责让数据从程序流向屏幕。文件流的用法跟它们几乎一样,只不过管道的另一头换成了磁盘上的具体文件。

在 C++ 中,如果想要使用文件流,我们需要先通过 #include <fstream> 引入头文件。其中,fstream 是 “file stream(文件流)” 的缩写。<fstream> 标准库提供了 3 种常用的文件流类型,如下表所示。

C++ 文件流类型
类型名称 英文全称 数据流向 主要用途
std::ifstream input file stream 文件 → 程序 读取文件
std::ofstream output file stream 程序 → 文件 写入文件
std::fstream file stream 文件 ↔ 程序 同时读写文件

怎么快速区分 std::ifstream 和 std::ofstream 呢?我们只需要记住一个原则:永远站在 “程序” 这一边看方向。

数据从文件读进程序叫做 “输入(input)”,所以读取文件使用 std::ifstream。数据从程序写到文件叫做 “输出(output)”,所以写入文件使用 std::ofstream。

在实际开发中,如果只需要读取文件,我们优先选择 std::ifstream;如果只需要写入文件,则优先选择 std::ofstream。把读和写的职责分开,不仅代码意图更清晰,编译器还能在编译阶段帮我们找出误写或误读的错误。

注意 很多初学的小伙伴习惯把 “往文件里写内容” 说成 “向文件输入”,结果误选了 std::ifstream。记住前缀 “i” 和 “o” 都是站在程序角度说的:读进程序使用 std::ifstream,写出到文件使用 std::ofstream,千万不要搞反了。

C++ 文件的写入与读取

只要熟悉了控制台的输入和输出,上手文件读写其实非常轻松。向 std::ofstream 写入数据时,我们直接使用 << 运算符即可。从 std::ifstream 读取数据时,可以使用 >> 运算符,也可以调用 std::getline() 函数读取一整行。

下面我们通过一个完整的例子,先动手跑通 “写入文件 → 关闭文件 → 读取文件” 的基本流程。

示例 1:写入并读取文件

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

int main() {
    // 1. 打开文件并写入两行文本
    std::ofstream outFile("lvye.txt");
    outFile << "欢迎来到绿叶网!\n";
    outFile << "https://www.lvyenet.com\n";
    outFile.close();    // 写完后先关闭文件

    // 2. 重新打开文件并读取内容
    std::ifstream inFile("lvye.txt");
    std::string line1;
    std::string line2;
    std::getline(inFile, line1);
    std::getline(inFile, line2);

    std::cout << "第 1 行:" << line1 << '\n';
    std::cout << "第 2 行:" << line2 << '\n';
    return 0;
}

运行结果如下。

第 1 行:欢迎来到绿叶网!
第 2 行:https://www.lvyenet.com

分析:

在这个例子中,第 1 步先通过 std::ofstream outFile("lvye.txt"); 打开文件。由于当前工作目录下原本没有 lvye.txt,程序会自动新建该文件。接着我们使用 << 写入两行文本,最后调用 outFile.close() 将文件关闭。

第 2 步通过 std::ifstream inFile("lvye.txt"); 重新打开刚才生成的文件,连续调用两次 std::getline() 把两行文字读入变量 line1 和 line2 并打印出来。

大家在本地运行完这段代码后,也可以去项目运行目录下找找新建的 lvye.txt,直接双击打开看看里面保存的文字。关于读取文件与写入文件的更多细节用法,我们会在后面两节中分别展开介绍。

打开文件与状态检查

在实际操作文件时,除了基本的读写语法,我们还需要掌握打开文件的写法、常用的打开模式,以及如何判断文件是否成功打开。

1. 打开文件的方式与模式

如果只写 std::ofstream file;,此时只是定义了一个空的文件流对象,还没有和磁盘上的任何文件建立关联。在 C++ 中,打开文件主要有 2 种方式:一种是在定义对象时直接传入文件路径,另一种是先定义对象、再调用 open() 成员函数打开。

语法:

文件流类型 file("文件路径");
文件流类型 file("文件路径", 打开模式);

说明:

其中,第 2 个参数 “打开模式” 是可选的,省略时会自动采用该文件流的默认模式。除了定义时直接打开,我们也可以先定义空对象再调用 open() 函数。下面 2 种方式是等价的。

// 方式 1(推荐)
std::ofstream file("lvye.txt");

// 方式 2
std::ofstream file;
file.open("lvye.txt");

在实际开发中,我们优先推荐使用方式 1,一步到位,代码更加紧凑直观。方式 2 则适合 “先定义好文件流对象,后面再根据条件决定打开哪个文件” 的场景。

当我们需要显式指定第 2 个参数时(比如 std::ofstream file("lvye.txt", std::ios::app);),常用的文件打开模式如下表所示。

常用文件打开模式
打开模式 英文原词 说明
std::ios::in input 以读取方式打开文件,是 std::ifstream 的默认模式
std::ios::out output 以写入方式打开文件,是 std::ofstream 的默认模式
std::ios::app append 追加模式,每次写入都添加到文件末尾
std::ios::trunc truncate 文件已存在时,打开会先清空原有内容
std::ios::binary binary 以二进制方式打开文件,未指定时默认为文本方式

上表中的模式小伙伴们先认个脸熟即可,不需要死记硬背。默认情况下,std::ifstream 自动采用 std::ios::in 模式,目标文件不存在时就会打开失败。而 std::ofstream 默认组合了 std::ios::out 与 std::ios::trunc 模式,目标文件不存在时会自动新建,已存在时则会先清空旧内容。

至于支持双向读写的 std::fstream,打开时通常需要使用 | 符号同时指定读写模式,比如写成 std::fstream rwFile("lvye.txt", std::ios::in | std::ios::out);。

不过这种双向模式要求目标文件必须已经存在,而且读取和写入共用同一个内部位置标记。刚写完直接去读,会因为位置停在文件末尾而读不到数据,所以平时把读和写分开交给 std::ifstream 与 std::ofstream 会省心很多。

2. 判断文件是否打开成功

在前面的示例 1 中,文件读写一切顺利。但在真实项目里,如果要读取的文件不存在、路径拼写错了或者没有访问权限,文件就会打开失败。因此在正式读写数据之前,我们需要先检查文件是否成功打开。

语法:

if (!file.is_open()) {
    // 处理打开失败
}

说明:

file.is_open() 会返回一个 bool 值:如果文件成功打开,返回 true;如果打开失败,则返回 false。配合逻辑非运算符 ! 写成 if (!file.is_open()),一旦发现打开失败就能及时给出提示并提前退出。

示例 2:判断文件是否打开成功

#include <fstream>
#include <iostream>

int main() {
    // 尝试打开不存在的文件
    std::ifstream file("no_such_file.txt");

    if (!file.is_open()) {
        std::cout << "文件打开失败\n";
        return 1;
    }

    std::cout << "文件打开成功\n";
    return 0;
}

运行结果如下。

文件打开失败

分析:

由于当前目录下并没有 "no_such_file.txt",file.is_open() 返回 false,!file.is_open() 条件成立,程序打印提示并通过 return 1; 提前退出。在 main() 函数中,返回 0 表示正常结束,返回非零整数表示异常退出。

除了调用 is_open(),我们也可以直接把文件流对象放进 if 条件中检查。在刚打开文件时,下面 2 种方式是等价的。

// 方式 1
if (!file.is_open()) {
    // 处理打开失败
}

// 方式 2
if (!file) {
    // 处理打开失败
}

is_open() 与 !file 的区别

在刚打开文件的那一刻,!file.is_open() 和 !file 的判断结果是一样的。但在后续读写数据的过程中,两者的侧重点有所不同。

file.is_open() 只关心 “文件流当前有没有关联上文件”,而 !file 判断的是 “文件流整体是否处于错误状态”。比如文件已经成功打开,但中途某次读取因格式不匹配而失败时,file.is_open() 依然为 true,!file 则会变为 true。

因此在实际开发中,我们通常在打开文件时使用 !file.is_open() 检查打开状态,而在读写数据后使用 !file 检查本次读写是否成功。

C++ 关闭文件

当文件操作结束后,我们需要把文件关闭以释放系统资源。在 C++ 中,关闭文件一般分为 2 种情况:一种是 “离开作用域自动关闭”,另一种是调用 close() 函数手动关闭。

1. 离开作用域自动关闭(推荐)

可能有些小伙伴会问:“每次用完文件之后,是不是都必须手动写一行 file.close() 去关闭它呢?” 在大多数普通场景下,其实并不需要。

这是因为文件流对象本身就是一个普通的 C++ 局部对象,遵循前面学过的 RAII 机制。当文件流对象离开它所在的花括号 {} 作用域时,析构函数会自动把缓冲区里的数据写入磁盘并关闭文件,比如下面这样:

{
    std::ofstream file("lvye.txt");
    if (file.is_open()) {
        file << "自动关闭测试\n";
    }
}    // 离开作用域,自动关闭文件

因此,如果一个函数只是单纯读取或写入某个文件,操作完函数就结束了,我们更推荐依赖离开作用域时的自动关闭。这样不仅代码更简洁,也不用担心中途提前 return 时漏掉关闭操作。

2. 使用 close() 手动关闭

既然文件流会自动关闭,那什么时候才需要我们手动调用 close() 函数呢?主要有 2 种情况:① 在同一个作用域里刚写完文件,紧接着马上又要读取这个文件(如示例 1);② 准备让同一个文件流对象先断开当前文件,再去打开另一个新文件。

语法:

file.close();

说明:

调用 file.close() 会立即将内存缓冲区中的数据刷新到磁盘,并断开当前文件流与文件的连接。关闭之后,我们就可以让该文件流对象再次调用 open() 函数去关联其他文件。

示例 3:关闭文件并切换新文件

#include <fstream>
#include <iostream>

int main() {
    std::ofstream file;

    // 打开并关闭第 1 个文件
    file.open("first.txt");
    if (file.is_open()) {
        file << "first file\n";
        std::cout << "第 1 个文件写入并关闭成功\n";
        file.close();
    }

    // 复用同一个流对象打开第 2 个文件
    file.open("second.txt");
    if (file.is_open()) {
        file << "second file\n";
        std::cout << "第 2 个文件写入并关闭成功\n";
        file.close();
    }

    return 0;
}

运行结果如下。

第 1 个文件写入并关闭成功
第 2 个文件写入并关闭成功

分析:

file.close(); 表示断开流对象 file 与 "first.txt" 的连接,这样后面我们才能继续复用同一个 file 对象调用 file.open("second.txt") 打开第 2 个文件。

小伙伴们要注意,如果一个文件流对象已经打开了某个文件,在关闭它之前不能直接调用 open() 去打开另一个文件。否则第 2 次打开会失败,还会导致该文件流进入错误状态。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号