3. 二进制文件的读写操作

fopen 函数打开文件时,第二个参数 mode(打开模式)中含有 b 标志时是二进制方式打开文件。当以二进制方式打开文件时,建议使用二进制文件读写相关的函数进行操作。

二进制文件的读/写操作

二进制文件读/写操作常用的函数有 freadfwrite

二进制文件读/写入相关的函数

函数
说明
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
从文件流 stream 中读取 size * nmemb 个字节存入 ptr 指向的缓冲区中。
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
将 ptr 指向的缓存区中的 size * nmemb 个字节写入文件 stream 中。

说明:

  1. size 是每个成员的长度,比如一个结构体的大小,nmemb 是要写入成员的个数。最终读写的总字节数是 size ✖️ nmemb 个字节数。
  2. sizenmemb 都是大于等于 1 的正整数。
  3. 读取时 ptr 指向的内存空间要大于等于 size ✖️ nmemb,否则可能会引起读/写越界。
  4. 使用以上两个函数需要包含头文件 stdio.h
  5. 如果读/写文件成功则返回操作数据块的 nmemb, 如果磁盘空间已满(写的情况)或文件到达文件尾(读的情况)则返回值可能小于 nmemb,甚至返回 0

示例:

将一个结构体数组的全部内容写入到文件中。

// filename: binary_write.c
#include <stdio.h>

struct student {
    char name[32];
    int age;
    int score;
};

int main(int argc, char * argv[]) {
    FILE * pf = NULL;  // 用于保存已经打开的文件
    struct student stus[2] = {{"zhangsan", 18, 100}, {"lisi", 19, 80}};
    int rw_value = 0;  // 用于保存读写文件的返回值

    printf("每个结构体的长度是: %ld\n", sizeof(struct student));
    printf("结构体的数据个数是: %ld\n", sizeof(stus)/sizeof(stus[0]));

    // 以二进制写的方式打开文件
    pf = fopen("mydata.txt", "wb");
    if (NULL == pf) {
        perror("打开文件 mydata.tx");
        return -1;
    }
    rw_value = fwrite(stus, sizeof(struct student),
                      sizeof(stus)/sizeof(stus[0]), pf);
    // 检查写入结果
    if (rw_value < sizeof(stus)/sizeof(stus[0])) {
        printf("写入文件的数据不够完整,可能都是数据!\n");
    }
    printf("fwrite returned: %d\n", rw_value);

    // 关闭文件
    fclose(pf);

    return 0;
}

编译和运行结果如下:

weimingze@mzstudio:~$ ls
binary_write.c
weimingze@mzstudio:~$ gcc -o binary_write binary_write.c
weimingze@mzstudio:~$ ./binary_write
每个结构体的长度是: 40
结构体的数据个数是: 2
fwrite returned: 2
weimingze@mzstudio:~$ ls
binary_write  binary_write.c  mydata.txt
weimingze@mzstudio:~$ ls -l mydata.txt
-rw-r--r-- 1 weimingze weimingze 80 12月  6 10:16 mydata.txt
weimingze@mzstudio:~$

从运行结果可知,每一个结构体变量 struct student 在内存中占用 40 个字节,数组中共有两个数据元素。调用函数 fwrite 返回值是 2,成功生成了 mydata.txt 文件。该文件的长度是 80 个字节,正好是内存中两个结构体占用的内存空间的大小,即二进制文件的写操作就是将内存中的内容复制到文件中。

我们使用 记事本 等文本编辑器打开 mydata.txt 文件时,发现除了部分字符串可见,其余都是乱码。这是因为以二进制方式保存在文件中的数据是无法解析成文字的内容,因此出现乱码。

注意事项:

  1. 使用二进制文件存储数据要注意字节序问题,如年龄成绩以小端字节序的形成存入文件中,又将此文件发送给默认是大端字节序的机器读取数据就可能会出错,因此二进制文件要明确规定字节序。
  2. 使用二进制文件存储数据要注意结构体字节对齐问题,通常我们使用 #pragma pack(1) 对结构体进行压缩以解决此问题。如果不压缩结构体,可能会引起不同计算机结构体对齐字节数不一致问题。
  3. 使用二进制文件存储字符串数据时,上述结构体的中的 name 数组长度是 40, 但实际可能只使用了前面的部分用来保存姓名。数组后面的不用数据也存入到了文件中。在实际开发中后面这部分数据可能会保存有密码等机密信息。因此会带来安全隐患。因此要保证所有写入的数据的正确性和安全性。比如可以在写入文件前将字符串后面的数据全部清零。

示例:

下面我们写程序将上述保存 mydata.txt 文件的内容读取出来,然后将信息打印到控制台终端中。

// filename: binary_read.c
#include <stdio.h>

struct student {
    char name[32];
    int age;
    int score;
};

// 定义数组的最大元素个数是 100
#define STU_LEN (100)

int main(int argc, char * argv[]) {
    FILE * pf = NULL;  // 用于保存已经打开的文件
    struct student stus[STU_LEN];
    int rw_value = 0;  // 用于保存读写文件的返回值
    int i;

    // 以二进制 读 的方式打开文件
    pf = fopen("mydata.txt", "rb");
    if (NULL == pf) {
        perror("打开文件 mydata.txt");
        return -1;
    }
    // 计划读取 100 个数据信息
    rw_value = fread(stus, sizeof(struct student), STU_LEN, pf);
    // 检查写入结果
    printf("fread returned: %d\n", rw_value);

    // 打印所有学生信息:
    for (i = 0; i < rw_value; i++) {
        printf("姓名: %s, 年龄: %d, 成绩: %d\n",
            stus[i].name, stus[i].age, stus[i].score);
    }

    // 关闭文件
    fclose(pf);

    return 0;
}

编译和运行结果如下:

weimingze@mzstudio:~$ ls
binary_read.c  mydata.txt
weimingze@mzstudio:~$ ls -l mydata.txt
-rw-r--r-- 1 weimingze weimingze 80 12月  6 10:16 mydata.txt
weimingze@mzstudio:~$ gcc -o binary_read binary_read.c
weimingze@mzstudio:~$ ls
binary_read  binary_read.c  mydata.txt
weimingze@mzstudio:~$ ./binary_read
fread returned: 2
姓名: zhangsan, 年龄: 18, 成绩: 10
姓名: lisi, 年龄: 19, 成绩: 80

从运行结果可知,我们原计划读取 100 个结构体数据,实际只读取到了两个结构体的数据。fread 返回 2。

实验

  1. 改写上述结构体 struct student 年龄(age)用一个字节表示,成绩(score)用 float 类型来表示。使用 #pragma pack(1) 压缩结构体后以二进制方式将数据保存到文件中。
  2. 写程序,读取上述文件中的内容,并能够打印文件中的数据。