用C语言文件流实现轻量级图书管理系统:从0到1的实战解析

引言

在C语言的学习过程中,文件操作是一个绕不开的核心技能。无论是保存用户数据、记录日志,还是实现小型系统,“如何将数据持久化到本地”都是必须解决的问题。今天,基于之前写的轻量级图书管理系统,通过文件流复现,带你深入理解C语言文件流的核心概念(如文件指针、文本/二进制模式、文件读写逻辑),并展示如何用文件流替代内存存储,解决小型系统的实际需求。


一、为什么选择文件流?——对比内存存储的局限性

在开发小型系统时,我们可能会先用数组或链表在内存中存储数据。但内存存储存在两个致命问题:

  1. 临时性:程序退出后,内存数据会被操作系统回收,无法长期保存。
  2. 容量限制:内存大小有限(如32位系统约4GB),无法处理大规模数据。

而文件流(File Stream)是操作系统提供的“持久化存储接口”,通过将数据写入磁盘文件,可以实现:

  • 数据持久化:程序退出后,数据仍保留在文件中,下次启动可重新加载。
  • 跨程序共享:文件是操作系统级别的资源,其他程序也能访问。
  • 灵活扩展:通过调整文件读写逻辑,可轻松支持新增字段或功能。

本文的图书管理系统将使用二进制文件流存储图书数据(结构体直接写入文件),兼顾效率与易用性。


二、核心设计:文件如何存储图书数据?

2.1 文件模式的选择:文本模式vs二进制模式

C语言中,文件操作通过fopen函数指定模式,常见的有:

  • 文本模式(如"r"/"w"):以字符形式读写,自动转换换行符(如Windows的\r 转Unix的 )。
  • 二进制模式(如"rb"/"wb"):以字节形式直接读写,不进行任何转换。

为什么选择二进制模式?
图书管理系统需要存储结构体Book(包含int numchar name[15]等字段)。若用文本模式,需手动将结构体序列化为字符串(如用|分隔字段),读取时再解析。这种方式不仅代码复杂,还可能因格式错误(如字段缺失)导致数据损坏。而二进制模式直接写入结构体的内存字节,读写逻辑更简单,效率更高(无需字符串转换)。

2.2 结构体与文件的交互:序列化与反序列化

在C语言中,结构体是内存中的一段连续字节。通过fwritefread函数,可以直接将结构体对象写入文件(序列化),或从文件读取字节并还原为结构体对象(反序列化)。

例如,写入一个Book结构体的代码:

1
2
3
4
Book book = {.num=1, .name="三体", .author="刘慈欣", .genre=SCIENCE_FICTION};
FILE *fp = fopen("book.dat", "wb"); // 二进制写模式
fwrite(&book, sizeof(Book), 1, fp); // 写入1个Book结构体的字节
fclose(fp);

读取时:

1
2
3
4
Book book;
FILE *fp = fopen("book.dat", "rb"); // 二进制读模式
fread(&book, sizeof(Book), 1, fp); // 从文件读取1个Book结构体的字节
fclose(fp);

注意:二进制模式要求结构体在内存中是连续存储的(无填充或对齐问题)。C语言默认会对结构体进行内存对齐(如char后填充3字节使int对齐到4字节边界),但fwritefread会按实际内存布局读写,因此只要读写时的结构体定义一致,数据不会出错。


三、关键代码解析:文件流的核心操作

3.1 文件打开与关闭:fopenfclose

fopen函数的原型是:

1
FILE *fopen(const char *filename, const char *mode);
  • filename:文件路径(如"book.dat")。
  • mode:打开模式(如"rb"表示二进制读,"wb"表示二进制写)。

关键模式说明

  • "rb":只读二进制模式(文件必须存在,否则返回NULL)。
  • "wb":只写二进制模式(文件不存在则创建,存在则清空内容)。
  • "ab+":读写二进制模式(文件不存在则创建,存在则追加内容到末尾)。

fclose函数用于关闭文件,释放系统资源。必须确保每次fopen都有对应的fclose,否则可能导致文件损坏或数据丢失。

3.2 写入文件:fwrite的用法

fwrite的原型是:

1
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:指向要写入数据的内存指针(如&book)。
  • size:单个元素的大小(如sizeof(Book))。
  • nmemb:要写入的元素个数(如1表示写入1个Book结构体)。
  • stream:文件指针(由fopen返回)。

示例:将1个Book结构体写入文件:

1
2
3
4
5
6
7
8
Book new_book = {.num=10, .name="C Primer Plus", .author="Stephen Prata", .genre=TECHNOLOGY};
FILE *fp = fopen("book.dat", "ab+"); // 追加模式(避免覆盖原有数据)
if (fp == NULL) {
perror("无法打开文件"); // 输出错误信息(如“权限不足”)
return;
}
fwrite(&new_book, sizeof(Book), 1, fp); // 写入1个Book结构体
fclose(fp);

3.3 读取文件:fread的用法

fread的原型是:

1
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:指向存储读取数据的内存指针(如&books数组)。
  • size:单个元素的大小(如sizeof(Book))。
  • nmemb:要读取的元素个数(如MAX_BOOKS表示最多读取100本)。
  • stream:文件指针。

示例:从文件读取所有图书数据:

1
2
3
4
5
6
7
8
Book books[MAX_BOOKS];
FILE *fp = fopen("book.dat", "rb");
if (fp == NULL) {
printf("文件不存在,将使用默认数据。\n");
return;
}
int cnt = fread(books, sizeof(Book), MAX_BOOKS, fp); // 读取最多100本
fclose(fp);

3.4 错误处理:避免程序崩溃

文件操作可能遇到多种错误(如文件不存在、磁盘空间不足),必须进行错误检查:

  • fopen返回NULL:说明文件无法打开(如路径错误、权限不足)。此时应输出错误信息(用perror函数)并终止相关操作。
  • fread/fwrite返回值异常:这两个函数返回实际读写的元素个数。若返回值小于预期(如fread返回0但文件未结束),可能是文件损坏或磁盘错误。

示例:安全的文件读取逻辑

1
2
3
4
5
6
7
8
9
10
11
12
FILE *fp = fopen("book.dat", "rb");
if (fp == NULL) {
perror("错误:无法打开文件");
return 1;
}
int cnt = fread(books, sizeof(Book), MAX_BOOKS, fp);
if (cnt == 0 && ferror(fp)) { // 检查是否因错误导致读取失败
perror("错误:读取文件失败");
fclose(fp);
return 1;
}
fclose(fp);

四、实战演示:用户操作与文件交互

4.1 添加一本《C Primer Plus》

假设当前文件book.dat中已有10本书,现在添加第11本《C Primer Plus》:

  1. 用户选择“输入新的书籍信息”(选项2)。
  2. 程序调用find_empty_num查找最小空缺序号(假设当前最大序号是10,返回11)。
  3. 用户输入书名、作者、类别(假设选3:科技)。
  4. 程序创建Book结构体并写入文件(fwrite)。
  5. 最后调用write_to_file将整个数组重新写入文件(覆盖原内容)。

关键代码逻辑(input_book函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int input_book(Book *books, int cnt) {
if (cnt >= MAX_BOOKS) {
puts("书籍数量已达上限(100本),无法继续添加。\n");
return cnt;
}
Book new_book;
new_book.num = find_empty_num(books, cnt); // 查找空缺序号
printf("请输入书籍名称: ");
scanf("%s", new_book.name);
printf("请输入书籍作者: ");
scanf("%s", new_book.author);
int g;
do {
printf("请输入类别编号(0:科幻 1:文学 2:历史 3:科技 4:其他): ");
scanf("%d", &g);
} while (g < 0 || g > 4);
new_book.genre = g;
books[cnt] = new_book; // 添加到数组末尾
return cnt + 1; // 数量加1
}

4.2 删除一本《C Primer Plus》

删除操作的核心是定位目标书籍并覆盖后续数据

  1. 用户选择“按序号删除书籍”(选项3),输入要删除的序号(如11)。
  2. 程序遍历数组找到该书籍的位置(假设索引为10)。
  3. 将该位置之后的所有书籍向前移动一位(覆盖被删除的书籍)。
  4. 最后调用write_to_file将更新后的数组写入文件。

关键代码逻辑(delete_by_num函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int delete_by_num(Book *books, int cnt, int num) {
for (int i = 0; i < cnt; i++) {
if (books[i].num == num) {
// 后续书籍前移,覆盖被删除的位置
for (int j = i; j < cnt - 1; j++) {
books[j] = books[j + 1];
}
printf("编号为 %d 的书籍已删除。\n", num);
return cnt - 1; // 数量减1
}
}
printf("未找到编号为 %d 的书籍,无法删除。\n", num);
return cnt;
}

4.3 用文本编辑器查看文件内容(二进制文件)

虽然二进制文件无法直接用文本编辑器阅读,但我们可以通过hexdump(Linux)或HxD(Windows)工具查看其字节结构,验证数据是否正确存储。例如,一个Book结构体的二进制存储可能如下(假设num=11name="C Primer Plus"author="Stephen Prata"genre=3):

1
2
3
00000000: 0000000b 43205072 696d657220 506c7573  ....C Primer Plu
00000010: 73005374 65706865 6e205072 61746100 s.Stephen Prata.
00000020: 03000000 ....

其中:

  • 前4字节是num=11(十六进制0b)。
  • 接下来15字节是name"C Primer Plus",不足15字节补\0)。
  • 接下来20字节是author"Stephen Prata",不足20字节补\0)。
  • 最后4字节是genre=3(十六进制03)。

五、总结与扩展

5.1 文件流的优缺点

优点 缺点
实现简单(无需数据库) 读写效率较低(适合小数据量)
数据持久化(程序退出后保留) 无索引功能(查询效率随数据量增加下降)
跨平台兼容(文件是通用资源) 需手动处理数据格式(如结构体对齐)

5.2 未来优化方向

  • 改用二进制模式提升速度:当前代码已使用二进制模式,若数据量极大(如10万本),可进一步优化读写逻辑(如批量读写)。
  • 增加索引功能:为numname字段建立索引(如用数组记录序号对应的文件偏移量),提升查询效率。
  • 数据校验:在写入文件时添加校验码(如CRC校验),防止文件损坏导致数据丢失。
  • 支持更多字段:扩展Book结构体(如添加出版时间、价格),并调整文件读写逻辑。

附录:完整代码片段(关键函数)

1
2
3
4
5
6
7
8
9
void write_to_file(Book *books, int cnt) {
FILE *fp = fopen("book.dat", "wb"); // 二进制写模式(清空原内容)
if (fp == NULL) {
perror("错误:无法打开文件写入");
return;
}
fwrite(books, sizeof(Book), cnt, fp); // 写入所有书籍数据
fclose(fp);
}
1
2
3
4
5
6
7
8
9
int read_from_file(Book *books) {
FILE *fp = fopen("book.dat", "rb"); // 二进制读模式(文件不存在返回NULL)
if (fp == NULL) {
return 0; // 文件不存在,返回0本
}
int cnt = fread(books, sizeof(Book), MAX_BOOKS, fp); // 读取最多100本
fclose(fp);
return cnt; // 返回实际读取的数量
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>

#define MAX_BOOKS 100

typedef enum Genre {
SCIENCE_FICTION, LITERATURE, HISTORY, TECHNOLOGY, OTHER
} Genre;

typedef struct Book {
int num;
char name[15];
char author[20];
Genre genre;
} Book;

const char *Genre_Zn(Genre g) {
const char *genres[] = { "科幻", "文学", "历史", "科技", "其他" };
return g >= 0 && g < 5 ? genres[g] : "未知";
}

// 按序号排序书籍(冒泡排序)
void sort_books(Book *books, int cnt) {
for (int i = 0; i < cnt - 1; i++)
for (int j = 0; j < cnt - i - 1; j++)
if (books[j].num > books[j + 1].num) {
Book tmp = books[j];
books[j] = books[j + 1];
books[j + 1] = tmp;
}
}

// 打印书籍信息
void print_books(Book *books, int cnt) {
sort_books(books, cnt);
puts("--------------------- 所有的书籍信息 ---------------------");
for (int i = 0; i < cnt; i++)
printf("编号:%-2d 书名:%-12s 作者:%-13s 类别:%-8s\n",
books[i].num, books[i].name, books[i].author, Genre_Zn(books[i].genre));
}

// 按类别查找书籍
void find_by_genre(Book *books, int cnt, Genre g) {
for (int i = 0; i < cnt; i++)
if (books[i].genre == g)
printf("编号:%-2d 书名:%-12s 作者:%-13s 类别:%-8s\n",
books[i].num, books[i].name, books[i].author, Genre_Zn(books[i].genre));
}

// 按序号查找书籍
void find_by_num(Book *books, int cnt, int num) {
for (int i = 0; i < cnt; i++)
if (books[i].num == num) {
printf("编号:%-2d 书名:%-12s 作者:%-13s 类别:%-8s\n",
books[i].num, books[i].name, books[i].author, Genre_Zn(books[i].genre));
return;
}
printf("未找到编号为 %d 的书籍。\n", num);
}

// 按序号删除书籍
int delete_by_num(Book *books, int cnt, int num) {
for (int i = 0; i < cnt; i++)
if (books[i].num == num) {
for (int j = i; j < cnt - 1; j++)
books[j] = books[j + 1];
printf("编号为 %d 的书籍已删除。\n", num);
return cnt - 1;
}
printf("未找到编号为 %d 的书籍,无法删除。\n", num);
return cnt;
}

// 写入文件
void write_to_file(Book *books, int cnt) {
FILE *fp = fopen("book.txt", "wb");
if (fp) {
fwrite(books, sizeof(Book), cnt, fp);
fclose(fp);
}
else {
perror("book.txt open");
return 0;
}
}

// 读取文件
int read_from_file(Book *books) {
FILE *fp = fopen("book.txt", "rb");
if (!fp) {
perror("book.txt open");
return 0;
}
int cnt = fread(books, sizeof(Book), MAX_BOOKS, fp);
fclose(fp);
return cnt;
}

// 查找最小空缺序号
int find_empty_num(Book *books, int cnt) {
bool used[MAX_BOOKS + 1] = { false };
for (int i = 0; i < cnt; i++)
if (books[i].num <= MAX_BOOKS) used[books[i].num] = true;
for (int i = 1; i <= MAX_BOOKS; i++)
if (!used[i]) return i;
// 若没有空缺,返回最大序号 + 1
int max = 0;
for (int i = 0; i < cnt; i++)
if (books[i].num > max) max = books[i].num;
return max + 1;
}

// 输入新书籍信息
int input_book(Book *books, int cnt) {
if (cnt >= MAX_BOOKS) {
puts("书籍数量已达上限,无法继续添加。");
return cnt;
}

Book new_book = { .num = find_empty_num(books, cnt) };
printf("请输入书籍名称: ");
scanf("%s", new_book.name);
printf("请输入书籍作者: ");
scanf("%s", new_book.author);

int g;
do {
printf("请输入书籍类别编号(0:科幻 1:文学 2:历史 3:科技 4:其他): ");
scanf("%d", &g);
} while (g < 0 || g > 4);
new_book.genre = g;

books[cnt] = new_book;
return cnt + 1;
}

int main() {
Book books[MAX_BOOKS];
int cnt = read_from_file(books);

if (!cnt) {
Book init_books[] = {
{1, "三体", "刘慈欣", SCIENCE_FICTION},
{2, "红楼梦", "曹雪芹", LITERATURE},
{3, "中国通史", "吕思勉", HISTORY},
{4, "时间简史", "史蒂芬_霍金", TECHNOLOGY},
{5, "围城", "钱钟书", LITERATURE},
{6, "傲慢与偏见", "简_奥斯汀", LITERATURE},
{7, "呼啸山庄", "艾米莉_勃朗特", LITERATURE},
{8, "活着", "余华", LITERATURE},
{9, "明朝那些事儿", "当年明月", HISTORY},
{10, "乌合之众", "古斯塔夫_勒庞", OTHER}
};
cnt = sizeof(init_books) / sizeof(Book);
for (int i = 0; i < cnt; i++) books[i] = init_books[i];
write_to_file(books, cnt);
}

int choice;
do {
print_books(books, cnt);
puts("\n请选择操作:");
puts("0: 按类别查找书籍");
puts("1: 按序号查找书籍");
puts("2: 输入新的书籍信息");
puts("3: 按序号删除书籍");
puts("4: 退出");
scanf("%d", &choice);

switch (choice) {
case 0: {
int g;
do {
puts("\n请输入书籍类别编号(0:科幻 1:文学 2:历史 3:科技 4:其他 5:返回上一级)");
scanf("%d", &g);
if (g != 5) find_by_genre(books, cnt, g);
} while (g != 5);
break;
}
case 1: {
int num;
printf("请输入要查找的书籍序号: ");
scanf("%d", &num);
find_by_num(books, cnt, num);
break;
}
case 2:
cnt = input_book(books, cnt);
write_to_file(books, cnt);
break;
case 3: {
int num;
printf("请输入要删除的书籍序号: ");
scanf("%d", &num);
cnt = delete_by_num(books, cnt, num);
write_to_file(books, cnt);
break;
}
case 4:
puts("退出程序。");
break;
default:
puts("无效的选择,请重新输入。");
}
} while (choice != 4);

return 0;
}

通过这个案例,我们不仅实现了图书管理的基本功能,更深入理解了C语言文件流的核心机制。文件流是C语言与外部世界交互的重要桥梁,掌握它后,你可以轻松实现日志记录、配置保存、数据导出等功能。下次遇到需要持久化存储的需求时,不妨试试文件流!