C++ 二进制文件读写

在计算机中,文件一般分为 “文本文件” 和 “二进制文件” 两大类。记事本文档、运行日志和配置文件都属于文本文件,它们会把数据转换成可读字符来保存。比如使用 << 把 int 整数 100000 写入文本文件时,存进去的是 '1' 和 5 个 '0',一共占 6 个字节。

而二进制文件则是直接把数据在内存中的原始字节保存下来。同样保存 int 整数 100000,二进制方式不需要转换成字符,直接按内存大小固定占 4 个字节。

像图片、音频、视频和游戏存档都属于二进制文件。在 C++ 中读写这类文件,我们需要在打开文件流时指定 std::ios::binary 模式,再配合 write() 和 read() 函数按字节读写。

使用 write() 写入二进制数据

在 C++ 中,想要向文件中写入二进制数据,我们需要先以 std::ios::binary 模式打开 std::ofstream,再调用它的 write() 成员函数。

语法:

std::ofstream file("文件路径", std::ios::binary);
file.write(数据起始地址, 字节数);

说明:

write() 函数接收以下 2 个参数:

  • 数据起始地址(必选):类型为 const char*,表示要写入的那块内存的起始地址。如果要写入的本身就是 char 数组,我们直接传入数组名即可。如果是 int、double 或结构体变量,则需要先把变量地址转换成 const char* 类型。
  • 字节数(必选):准备往文件里写入多少个字节,通常直接使用 sizeof 运算符来获取,比如 sizeof(data)。

我们先从最直观的 char 数组看起。因为每个 char 本身恰好占 1 个字节,不需要转换地址类型,很适合用来观察二进制写入的特点。

示例 1:将字节数组写入文件

#include <fstream>
#include <iostream>

int main() {
    // 准备含空字符的 5 字节数据
    const char data[5] = {'A', 'B', '\0', 'C', 'D'};

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

    // 写入 5 个字节
    file.write(data, sizeof(data));
    if (!file) {
        std::cout << "文件写入失败\n";
        return 1;
    }

    std::cout << "共写入字节数:" << sizeof(data) << '\n';
    return 0;
}

运行结果如下。

共写入字节数:5

分析:

file.write(data, sizeof(data)); 表示从 data 数组的起始地址开始,连续把 sizeof(data) 算出的 5 个字节写入 sample.bin 文件中。

这里最关键的细节是数组中间的空字符 '\0'。学习 C 风格字符串时我们知道,如果按普通文本方式执行 file << data;,程序一旦遇到 '\0' 就会认为字符串已经结束,最终只会写入 'A' 和 'B' 两个字符。

而二进制的 write() 函数不管里面是不是 '\0',只认 “起始地址” 和 “字节数”。因此 ASCII 码依次为 65、66、0、67、68 的这 5 个字节都会完整存入文件中。

使用 read() 读取二进制数据

读取二进制文件的步骤与写入正好相反。我们先以 std::ios::binary 模式打开 std::ifstream,准备好用来接收数据的数组或变量,再调用 read() 成员函数。

语法:

std::ifstream file("文件路径", std::ios::binary);
file.read(缓冲区起始地址, 字节数);

说明:

read() 函数接收以下 2 个参数:

  • 缓冲区起始地址(必选):类型为 char*,表示用来接收数据的内存起始地址。由于程序需要向这块内存中写入读回的数据,所以这里不能加 const。
  • 字节数(必选):准备从文件中连续读取的字节数,通常同样使用 sizeof 来计算。

示例 2:读取字节数组

#include <fstream>
#include <iostream>

int main() {
    // 1. 准备 5 字节测试文件
    const char initData[5] = {'A', 'B', '\0', 'C', 'D'};
    std::ofstream outFile("sample.bin", std::ios::binary);
    outFile.write(initData, sizeof(initData));
    outFile.close();

    // 2. 读取 5 个字节到 buffer 数组
    char buffer[5]{};
    std::ifstream inFile("sample.bin", std::ios::binary);
    if (!inFile.is_open()) {
        std::cout << "文件打开失败\n";
        return 1;
    }

    inFile.read(buffer, sizeof(buffer));

    // 3. 将每个字节转为整数输出
    std::cout << "读取到的字节数值:";
    for (unsigned char byte : buffer) {
        std::cout << static_cast<int>(byte) << ' ';
    }
    std::cout << '\n';
    return 0;
}

运行结果如下。

读取到的字节数值:65 66 0 67 68

分析:

在这个例子中,我们先向 sample.bin 写入了 5 个字节,接着定义 char buffer[5]{}; 将接收数组初始化为 0,最后调用 inFile.read(buffer, sizeof(buffer)); 把 5 个字节连续读入 buffer 中。

由于第 3 个字节是不可见的空字符 '\0',直接按字符打印在屏幕上看不见,所以我们在循环里通过 static_cast<int>(byte) 把每个字节转成整数 ASCII 码再输出。这里使用 unsigned char 接收每个字节,是为了保证数值落在 0~255 之间,避免部分平台把大于 127 的字节当成负数。

从输出结果 65 66 0 67 68 可以看到,中间数值为 0 的空字符并没有截断读取,后面的 'C' 和 'D' 也都被完整读回来了。

在实际开发中,除了读写 char 数组,我们也经常需要把 int、double 或简单的结构体直接按二进制存入文件。

为什么读写结构体时需要做指针转换呢?因为在 C++ 中 1 个 char 固定占 1 个字节,所以 write() 和 read() 统一使用 const char* 或 char* 来表示单字节内存起点。

而对结构体变量取地址得到的类型是 结构体名*,我们需要借助 reinterpret_cast 把它转换成字节指针。这个转换关键字会在后面的类型转换章节详细介绍,这里大家先把它当成固定写法记住即可。

示例 3:读写简单结构体

#include <fstream>
#include <iostream>

struct Player {
    int id;
    double score;
};

int main() {
    Player p1 = {101, 98.5};

    // 1. 将结构体按二进制写入文件
    std::ofstream out("player.bin", std::ios::binary);
    if (!out.is_open()) {
        std::cout << "写入文件打开失败\n";
        return 1;
    }
    out.write(reinterpret_cast<const char*>(&p1), sizeof(p1));
    out.close();

    // 2. 从二进制文件读回结构体
    Player p2{};
    std::ifstream in("player.bin", std::ios::binary);
    if (!in.is_open()) {
        std::cout << "读取文件打开失败\n";
        return 1;
    }
    in.read(reinterpret_cast<char*>(&p2), sizeof(p2));

    std::cout << "玩家编号:" << p2.id << '\n';
    std::cout << "玩家得分:" << p2.score << '\n';
    return 0;
}

运行结果如下。

玩家编号:101
玩家得分:98.5

分析:

初学的小伙伴看到 out.write(reinterpret_cast<const char*>(&p1), sizeof(p1)); 这句代码可能会觉得有点长,其实拆成以下 3 个部分就很好理解了:

  • &p1:取出结构体变量 p1 在内存中的起始地址,此时它的指针类型是 Player*。
  • reinterpret_cast<const char*>(...):把 p1 的起始地址看作一串连续字节的起点,满足 write() 按单字节写入的参数要求。读取时同理,转为可读写的 char* 即可。
  • sizeof(p1):计算出结构体 p1 占用的总字节数,明确告诉函数一共要搬运多少个字节。

注意 无论调用 write() 还是 read(),打开二进制文件时都不要漏掉 std::ios::binary 模式。如果不加这个模式,Windows 系统会按文本方式自动转换换行符,把数值为 10 的字节替换成 13 和 10 两个字节,导致二进制数据发生错位。

二进制读写的注意事项

在使用 read() 和 write() 操作二进制文件时,有 2 个容易踩坑的边界问题需要小伙伴们特别注意。

1. 读取字节数不足与 gcount()

对于 in.read(buffer, 8) 来说,它的目标是必须读满 8 个字节。如果文件里一共只有 5 个字节,读完这 5 个字节就碰上了文件末尾,文件流就会因为没读满而进入失败状态,此时 !in 为 true。

如果我们想知道刚才那一次实际读进来了多少个字节,可以调用文件流的 gcount() 成员函数。其中,gcount 可以理解为 “get count(获取读取计数)”。

示例 4:获取实际读取字节数

#include <fstream>
#include <iostream>

int main() {
    // 1. 准备 5 字节测试文件
    const char initData[5] = {'A', 'B', '\0', 'C', 'D'};
    std::ofstream out("sample.bin", std::ios::binary);
    out.write(initData, sizeof(initData));
    out.close();

    // 2. 尝试读取 8 个字节
    char buffer[8]{};
    std::ifstream in("sample.bin", std::ios::binary);
    if (!in.is_open()) {
        std::cout << "文件打开失败\n";
        return 1;
    }

    in.read(buffer, sizeof(buffer));
    if (!in) {
        std::cout << "未读满 8 个字节\n";
        std::cout << "实际读取字节数:" << in.gcount() << '\n';
    }

    return 0;
}

运行结果如下。

未读满 8 个字节
实际读取字节数:5

分析:

由于 sample.bin 只有 5 个字节,而 sizeof(buffer) 为 8,执行 in.read(buffer, sizeof(buffer)); 后差了 3 个字节没读满,因此 if (!in) 条件成立。此时我们调用 in.gcount(),就能准确查出本次实际读入了 5 个字节。

2. 动态内存对象的读写限制

前面的示例 3 直接把 Player 结构体整块写进了二进制文件,但并不是所有结构体都能这样操作。如果结构体中包含 std::string 或 std::vector 成员,下面这种整块写入的写法就是错误的。

struct Student {
    int id;
    std::string name;    // 内部包含堆内存指针
};

Student stu = {101, "Jack"};
out.write(reinterpret_cast<const char*>(&stu), sizeof(stu));

为什么包含 std::string 或 std::vector 的结构体不能直接通过 write() 整块写入文件呢?原因主要有以下 2 点:

  • 内部保存的是指针地址:std::string 和 std::vector 的实际元素存放在堆内存中,结构体内部只保存了一个内存地址。如果我们直接整块写进文件,存进去的只是当前运行时的地址编号,真实数据并没有写入,下次读回这个失效地址就会导致程序崩溃。
  • 存在内存对齐与平台差异:不同编译器或操作系统对结构体的内存对齐填充字节、多字节整数的排列顺序可能不同,换一台设备读取时容易发生数据错位。

因此,只有仅包含基本数据类型或定长数组的简单结构体,才适合直接整块按二进制读写。如果结构体中包含 std::string 或 std::vector 等动态内存成员,小伙伴们可以把各个字段拆开逐一读写,或者改用后面章节介绍的 CSV、JSON 等文本格式来保存。

上一篇:

下一篇:

给站长反馈

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

邮箱:lvyenet@vip.qq.com

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