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

【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为例文件类型存储方式占用空间文本文件存储字符123455字节二进制文件存储 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文件指针返回值成功写入的元素个数通常等于nmemb3.2 fread二进制读取csize_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获取当前读写位置clong ftell(FILE *stream);功能返回文件当前位置相对于文件开头的字节偏移量返回值成功返回偏移量失败返回 -1Lc#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移动文件位置指针cint fseek(FILE *stream, long offset, int whence);功能移动文件位置指针参数offset偏移量字节数whence起始位置取以下宏SEEK_SET文件开头SEEK_CUR当前位置SEEK_END文件末尾返回值成功返回0失败返回非0c#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重置到文件开头cvoid rewind(FILE *stream);等价于fseek(stream, 0, SEEK_SET)。crewind(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 指针类型转换错误cint 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 结构体包含指针ctypedef 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 文件定位越界cfseek(fp, -10, SEEK_SET); /* 错误不能定位到文件开头之前 */ fseek(fp, 1000, SEEK_END); /* 可以但写入会扩展文件 */6.6 忽略返回值检查cfread(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 与 fwritefread(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结构体含指针时需特殊处理跨平台注意字节序检查返回值处理错误和文件结束