【C语言程序设计】第36篇:二进制文件的读写

1 引言

考虑这样一个需求:存储一个包含1000个学生的数据文件,每个学生有姓名、学号、成绩等字段。如果用文本文件存储,需要将每个数字转换为字符串,占用空间大,读写时还要进行格式转换,效率低。而二进制文件可以直接将内存中的数据原样写入,读写速度快,占用空间小。

c

复制代码
#include <stdio.h>

typedef struct {
    char name[20];
    int id;
    float score;
} Student;

int main(void)
{
    Student stu = {"张三", 1001, 88.5};
    FILE *fp = fopen("student.dat", "wb");
    if (fp == NULL) return 1;
    
    /* 直接写入结构体数据 */
    fwrite(&stu, sizeof(Student), 1, fp);
    
    fclose(fp);
    return 0;
}

二进制文件写出的内容无法直接用文本编辑器查看,但它正是计算机内部存储数据的真实形式。本章我们将深入学习二进制文件的操作。


2 二进制文件与文本文件的对比

2.1 存储形式对比

以整数 12345 为例:

文件类型 存储方式 占用空间
文本文件 存储字符 '1' '2' '3' '4' '5' 5字节
二进制文件 存储 int 的二进制表示 0x00003039(小端序) 4字节

2.2 读写效率对比

c

复制代码
/* 文本方式:需要格式转换 */
fprintf(fp, "%d", n);   /* 整数 → 字符串 */
fscanf(fp, "%d", &n);   /* 字符串 → 整数 */

/* 二进制方式:直接复制内存 */
fwrite(&n, sizeof(int), 1, fp);  /* 直接写入内存中的二进制 */
fread(&n, sizeof(int), 1, fp);   /* 直接读入内存 */

2.3 优缺点对比

方面 文本文件 二进制文件
可读性 可直接查看 需要专用工具
空间效率 较低 较高
读写速度 较慢(需转换) 较快(直接拷贝)
跨平台 好(换行符可转换) 需注意字节序
适用场景 配置文件、日志 大数据存储、程序数据

3 fread 和 fwrite 函数

3.1 fwrite:二进制写入

c

复制代码
#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 功能:将内存中的数据块写入文件

  • 参数

    • ptr:要写入的数据的起始地址

    • size:每个数据元素的大小(字节)

    • nmemb:要写入的元素个数

    • stream:文件指针

  • 返回值 :成功写入的元素个数(通常等于 nmemb

3.2 fread:二进制读取

c

复制代码
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 功能:从文件读取数据块到内存

  • 参数 :同 fwrite

  • 返回值 :成功读取的元素个数(可能小于 nmemb,如文件结束)

3.3 基本使用示例

c

复制代码
#include <stdio.h>

int main(void)
{
    int data[] = {1, 2, 3, 4, 5};
    int buffer[5];
    FILE *fp;
    
    /* 写入 */
    fp = fopen("data.bin", "wb");
    if (fp == NULL) return 1;
    fwrite(data, sizeof(int), 5, fp);
    fclose(fp);
    
    /* 读取 */
    fp = fopen("data.bin", "rb");
    if (fp == NULL) return 1;
    fread(buffer, sizeof(int), 5, fp);
    fclose(fp);
    
    /* 验证 */
    for (int i = 0; i < 5; i++) {
        printf("%d ", buffer[i]);  /* 输出 1 2 3 4 5 */
    }
    printf("\n");
    
    return 0;
}

3.4 读写结构体数组

c

复制代码
#include <stdio.h>
#include <string.h>

typedef struct {
    char name[20];
    int id;
    float score;
} Student;

int save_students(Student students[], int n, const char *filename)
{
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) return -1;
    
    size_t written = fwrite(students, sizeof(Student), n, fp);
    fclose(fp);
    
    return written == n ? 0 : -1;
}

int load_students(Student students[], int max, const char *filename)
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return -1;
    
    size_t read = fread(students, sizeof(Student), max, fp);
    fclose(fp);
    
    return read;
}

int main(void)
{
    Student students[] = {
        {"张三", 1001, 88.5},
        {"李四", 1002, 92.0},
        {"王五", 1003, 78.5}
    };
    int n = 3;
    Student buffer[10];
    
    /* 保存 */
    if (save_students(students, n, "students.dat") == 0) {
        printf("保存成功\n");
    }
    
    /* 读取 */
    int count = load_students(buffer, 10, "students.dat");
    if (count > 0) {
        printf("读取到 %d 条记录:\n", count);
        for (int i = 0; i < count; i++) {
            printf("%s %d %.1f\n", 
                   buffer[i].name, buffer[i].id, buffer[i].score);
        }
    }
    
    return 0;
}

3.5 读写指针变量的注意事项

c

复制代码
/* 错误:不能直接写入指针 */
typedef struct {
    char *name;  /* 指针,不是实际数据 */
    int id;
} BadStudent;

BadStudent s;
s.name = (char*)malloc(20);
strcpy(s.name, "张三");

/* 错误!只写入了指针值(地址),不是字符串内容 */
fwrite(&s, sizeof(BadStudent), 1, fp);

/* 正确做法:先写入长度,再写入实际数据 */
fwrite(s.name, strlen(s.name) + 1, 1, fp);

4 文件定位

4.1 ftell:获取当前读写位置

c

复制代码
long ftell(FILE *stream);
  • 功能:返回文件当前位置(相对于文件开头的字节偏移量)

  • 返回值:成功返回偏移量,失败返回 -1L

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;
    
    fgetc(fp);                     /* 读取一个字符 */
    long pos = ftell(fp);
    printf("当前位置:%ld\n", pos);  /* 输出 1 */
    
    fclose(fp);
    return 0;
}

4.2 fseek:移动文件位置指针

c

复制代码
int fseek(FILE *stream, long offset, int whence);
  • 功能:移动文件位置指针

  • 参数

    • offset:偏移量(字节数)

    • whence:起始位置,取以下宏:

      • SEEK_SET:文件开头

      • SEEK_CUR:当前位置

      • SEEK_END:文件末尾

  • 返回值:成功返回0,失败返回非0

c

复制代码
#include <stdio.h>

int main(void)
{
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) return 1;
    
    /* 定位到文件开头 */
    fseek(fp, 0, SEEK_SET);
    
    /* 跳过前10个字节 */
    fseek(fp, 10, SEEK_CUR);
    
    /* 定位到文件末尾前5个字节 */
    fseek(fp, -5, SEEK_END);
    
    fclose(fp);
    return 0;
}

4.3 rewind:重置到文件开头

c

复制代码
void rewind(FILE *stream);

等价于 fseek(stream, 0, SEEK_SET)

c

复制代码
rewind(fp);  /* 回到文件开头 */

4.4 获取文件大小

c

复制代码
#include <stdio.h>

long get_file_size(const char *filename)
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return -1;
    
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);
    
    return size;
}

int main(void)
{
    long size = get_file_size("data.bin");
    if (size >= 0) {
        printf("文件大小:%ld 字节\n", size);
    }
    return 0;
}

5 应用实例

5.1 动态数组的二进制存储

c

复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    int size;
    int capacity;
} DynamicArray;

/* 保存动态数组到文件 */
int save_array(DynamicArray *arr, const char *filename)
{
    FILE *fp = fopen(filename, "wb");
    if (fp == NULL) return -1;
    
    /* 先保存数组大小 */
    fwrite(&arr->size, sizeof(int), 1, fp);
    
    /* 再保存实际数据 */
    fwrite(arr->data, sizeof(int), arr->size, fp);
    
    fclose(fp);
    return 0;
}

/* 从文件加载动态数组 */
int load_array(DynamicArray *arr, const char *filename)
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return -1;
    
    int size;
    fread(&size, sizeof(int), 1, fp);
    
    arr->data = (int*)malloc(size * sizeof(int));
    if (arr->data == NULL) {
        fclose(fp);
        return -1;
    }
    
    fread(arr->data, sizeof(int), size, fp);
    arr->size = size;
    arr->capacity = size;
    
    fclose(fp);
    return 0;
}

int main(void)
{
    DynamicArray arr;
    arr.size = 5;
    arr.capacity = 10;
    arr.data = (int*)malloc(10 * sizeof(int));
    for (int i = 0; i < 5; i++) {
        arr.data[i] = i * i;
    }
    
    save_array(&arr, "array.dat");
    printf("保存成功\n");
    
    DynamicArray arr2 = {NULL, 0, 0};
    load_array(&arr2, "array.dat");
    
    for (int i = 0; i < arr2.size; i++) {
        printf("%d ", arr2.data[i]);  /* 输出 0 1 4 9 16 */
    }
    printf("\n");
    
    free(arr.data);
    free(arr2.data);
    return 0;
}

5.2 随机访问文件记录

c

复制代码
#include <stdio.h>
#include <string.h>

typedef struct {
    int id;
    char name[50];
    float salary;
} Employee;

/* 写入记录到指定位置(位置从0开始) */
int write_record(const char *filename, int pos, Employee *emp)
{
    FILE *fp = fopen(filename, "rb+");
    if (fp == NULL) return -1;
    
    /* 定位到指定记录 */
    fseek(fp, pos * sizeof(Employee), SEEK_SET);
    
    fwrite(emp, sizeof(Employee), 1, fp);
    fclose(fp);
    return 0;
}

/* 读取指定位置的记录 */
int read_record(const char *filename, int pos, Employee *emp)
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return -1;
    
    fseek(fp, pos * sizeof(Employee), SEEK_SET);
    
    if (fread(emp, sizeof(Employee), 1, fp) != 1) {
        fclose(fp);
        return -1;
    }
    
    fclose(fp);
    return 0;
}

/* 追加记录到末尾 */
int append_record(const char *filename, Employee *emp)
{
    FILE *fp = fopen(filename, "ab");
    if (fp == NULL) return -1;
    
    fwrite(emp, sizeof(Employee), 1, fp);
    fclose(fp);
    return 0;
}

/* 获取记录总数 */
int get_record_count(const char *filename)
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return 0;
    
    fseek(fp, 0, SEEK_END);
    long size = ftell(fp);
    fclose(fp);
    
    return size / sizeof(Employee);
}

int main(void)
{
    Employee e1 = {1001, "张三", 8000.0};
    Employee e2 = {1002, "李四", 9000.0};
    Employee e3 = {1003, "王五", 7500.0};
    
    /* 写入三条记录 */
    append_record("employees.dat", &e1);
    append_record("employees.dat", &e2);
    append_record("employees.dat", &e3);
    
    /* 修改第二条记录 */
    Employee e2_new = {1002, "李四", 9500.0};
    write_record("employees.dat", 1, &e2_new);
    
    /* 读取并显示所有记录 */
    int count = get_record_count("employees.dat");
    for (int i = 0; i < count; i++) {
        Employee emp;
        if (read_record("employees.dat", i, &emp) == 0) {
            printf("%d %s %.2f\n", emp.id, emp.name, emp.salary);
        }
    }
    
复制代码
    return 0;
}

6 常见错误与注意事项

6.1 忘记以二进制模式打开

c

复制代码
/* 错误:文本模式下换行符可能被转换,破坏二进制数据 */
FILE *fp = fopen("data.bin", "w");
fwrite(data, sizeof(int), 5, fp);

/* 正确 */
FILE *fp = fopen("data.bin", "wb");

6.2 指针类型转换错误

c

复制代码
int data[10];
fwrite(data, sizeof(int), 10, fp);  /* 正确,data 是地址 */

int *p = data;
fwrite(p, sizeof(int), 10, fp);     /* 正确 */

/* 错误:多了一层 & */
fwrite(&data, sizeof(int), 10, fp);  /* 写入的是整个数组的地址,错误! */

6.3 结构体包含指针

c

复制代码
typedef struct {
    char *name;  /* 指针,不是实际数据 */
} Bad;

Bad s;
s.name = "Hello";
fwrite(&s, sizeof(Bad), 1, fp);  /* 错误!只写入了指针值,不是字符串 */

6.4 跨平台字节序问题

不同系统可能使用不同的字节序(大端/小端),直接交换二进制文件可能出问题。

c

复制代码
/* 写入前转换为网络字节序(大端) */
uint32_t n = htonl(12345);  /* 主机序 → 网络序 */
fwrite(&n, sizeof(uint32_t), 1, fp);

/* 读取后转换回主机序 */
uint32_t n;
fread(&n, sizeof(uint32_t), 1, fp);
n = ntohl(n);  /* 网络序 → 主机序 */

6.5 文件定位越界

c

复制代码
fseek(fp, -10, SEEK_SET);  /* 错误!不能定位到文件开头之前 */
fseek(fp, 1000, SEEK_END);  /* 可以,但写入会扩展文件 */

6.6 忽略返回值检查

c

复制代码
fread(buffer, sizeof(int), 100, fp);  /* 可能只读取了50个,文件结束 */
/* 应该检查返回值 */
size_t read = fread(buffer, sizeof(int), 100, fp);
if (read < 100) {
    if (feof(fp)) printf("文件结束\n");
    else if (ferror(fp)) printf("读取错误\n");
}

7 本章小结

本章系统介绍了二进制文件的读写操作:

1. fread 与 fwrite

  • fread(ptr, size, nmemb, fp):读取数据块

  • fwrite(ptr, size, nmemb, fp):写入数据块

  • 直接读写内存中的二进制表示,效率高

2. 数据块读写原理

  • 参数:地址、元素大小、元素个数、文件指针

  • 返回值:实际读写成功的元素个数

  • 适合读写结构体数组等连续内存数据

3. 文件定位函数

  • ftell(fp):获取当前位置(字节偏移)

  • fseek(fp, offset, whence):移动位置

    • SEEK_SET:文件开头

    • SEEK_CUR:当前位置

    • SEEK_END:文件末尾

  • rewind(fp):重置到文件开头

4. 应用场景

  • 保存/加载结构体数组

  • 随机访问文件记录

  • 获取文件大小

  • 动态数据的持久化

5. 注意事项

  • 必须用二进制模式打开("rb"/"wb"

  • 结构体含指针时需特殊处理

  • 跨平台注意字节序

  • 检查返回值,处理错误和文件结束

相关推荐
ZPC82102 小时前
OLOv11 + 深度相机的方案实现高精度3D定位
人工智能·数码相机·算法·机器人
子非鱼@Itfuture2 小时前
try-catch和try-with-resources区别是什么?try{}catch(){}和try(){}catch(){}有什么好处?
java·开发语言
_日拱一卒2 小时前
LeetCode:字母异位词分组
算法·leetcode·职场和发展
Dfreedom.2 小时前
机器学习经典算法全景解析与演进脉络(监督学习篇)
人工智能·学习·算法·机器学习·监督学习
Amumu121382 小时前
Js:内置对象
开发语言·前端·javascript
2301_807367192 小时前
C++代码风格检查工具
开发语言·c++·算法
Morwit2 小时前
*【力扣hot100】 215. 数组中的第K个最大元素
数据结构·c++·算法·leetcode·职场和发展
奔袭的算法工程师2 小时前
用AI写天线阵列排布算法
人工智能·算法·信号处理
ab1515172 小时前
3.20二刷基础121、127,完成进阶61、62
数据结构·算法·排序算法