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") -
结构体含指针时需特殊处理
-
跨平台注意字节序
-
检查返回值,处理错误和文件结束