C: 综合项目:图书管理系统
造房子不能只学搬砖——现在该把所有技能组合起来,从地基到屋顶完整地盖一栋楼了。
1. 需求分析
我们要开发一个命令行图书管理系统,功能如下:
- 添加图书(书名、作者、ISBN、价格)
- 删除图书(按ISBN)
- 修改图书信息
- 查询图书(按书名或ISBN)
- 显示所有图书
- 数据保存到文件(持久化)
- 从文件加载数据
2. 数据结构设计
图书信息用一个结构体表示,所有图书用动态数组管理:
C
typedef struct {
char isbn[14];
char title[128];
char author[64];
double price;
} Book;
typedef struct {
Book *books;
int count;
int capacity;
} Library;
Library 管理一个动态数组:books 是数据指针,count 是当前数量,capacity 是已分配容量。当 count 达到 capacity 时自动扩容。
3. 项目文件组织
C
bookmanager/
├── Makefile
├── main.c
├── library.h
├── library.c
├── storage.h
└── storage.c
library.h/library.c:图书管理的核心逻辑(增删改查)storage.h/storage.c:文件读写main.c:主程序入口和用户界面Makefile:构建脚本
4. library.h
C
#ifndef LIBRARY_H
#define LIBRARY_H
typedef struct {
char isbn[14];
char title[128];
char author[64];
double price;
} Book;
typedef struct {
Book *books;
int count;
int capacity;
} Library;
void library_init(Library *lib);
void library_free(Library *lib);
int library_add(Library *lib, const Book *book);
int library_remove(Library *lib, const char *isbn);
Book *library_find_by_isbn(Library *lib, const char *isbn);
void library_find_by_title(Library *lib, const char *keyword,
Book **results, int *result_count);
int library_update(Library *lib, const char *isbn, const Book *new_info);
void library_list(const Library *lib);
#endif
5. library.c
C
#include "library.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int ensure_capacity(Library *lib) {
if (lib->count < lib->capacity) {
return 1;
}
int new_cap = lib->capacity == 0 ? 4 : lib->capacity * 2;
Book *new_books = realloc(lib->books, new_cap * sizeof(Book));
if (new_books == NULL) {
return 0;
}
lib->books = new_books;
lib->capacity = new_cap;
return 1;
}
void library_init(Library *lib) {
lib->books = NULL;
lib->count = 0;
lib->capacity = 0;
}
void library_free(Library *lib) {
free(lib->books);
lib->books = NULL;
lib->count = 0;
lib->capacity = 0;
}
int library_add(Library *lib, const Book *book) {
if (!ensure_capacity(lib)) {
return 0;
}
lib->books[lib->count] = *book;
lib->count++;
return 1;
}
int library_remove(Library *lib, const char *isbn) {
for (int i = 0; i < lib->count; i++) {
if (strcmp(lib->books[i].isbn, isbn) == 0) {
for (int j = i; j < lib->count - 1; j++) {
lib->books[j] = lib->books[j + 1];
}
lib->count--;
return 1;
}
}
return 0;
}
Book *library_find_by_isbn(Library *lib, const char *isbn) {
for (int i = 0; i < lib->count; i++) {
if (strcmp(lib->books[i].isbn, isbn) == 0) {
return &lib->books[i];
}
}
return NULL;
}
void library_find_by_title(Library *lib, const char *keyword,
Book **results, int *result_count) {
*result_count = 0;
for (int i = 0; i < lib->count; i++) {
if (strstr(lib->books[i].title, keyword) != NULL) {
results[*result_count] = &lib->books[i];
(*result_count)++;
}
}
}
int library_update(Library *lib, const char *isbn, const Book *new_info) {
Book *existing = library_find_by_isbn(lib, isbn);
if (existing == NULL) {
return 0;
}
*existing = *new_info;
return 1;
}
void library_list(const Library *lib) {
if (lib->count == 0) {
printf("图书馆中没有图书。\n");
return;
}
printf("%-14s %-30s %-20s %-8s\n", "ISBN", "书名", "作者", "价格");
printf("--------------------------------------------------------------\n");
for (int i = 0; i < lib->count; i++) {
printf("%-14s %-30s %-20s %-8.2f\n",
lib->books[i].isbn,
lib->books[i].title,
lib->books[i].author,
lib->books[i].price);
}
printf("共 %d 本图书\n", lib->count);
}
💡
ensure_capacity 是内部函数,用 static 修饰限制作用域在本文件内。动态数组扩容策略是翻倍——这是标准做法,保证均摊 O(1) 的插入效率。
6. storage.h
C
#ifndef STORAGE_H
#define STORAGE_H
#include "library.h"
int storage_save(const Library *lib, const char *filename);
int storage_load(Library *lib, const char *filename);
#endif
7. storage.c
C
#include "storage.h"
#include <stdio.h>
#include <string.h>
int storage_save(const Library *lib, const char *filename) {
FILE *fp = fopen(filename, "w");
if (fp == NULL) {
return 0;
}
for (int i = 0; i < lib->count; i++) {
fprintf(fp, "%s|%s|%s|%.2f\n",
lib->books[i].isbn,
lib->books[i].title,
lib->books[i].author,
lib->books[i].price);
}
fclose(fp);
return 1;
}
int storage_load(Library *lib, const char *filename) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
return 1;
}
Book book;
while (fscanf(fp, "%13[^|]|%127[^|]|%63[^|]|%lf\n",
book.isbn, book.title, book.author, &book.price) == 4) {
if (!library_add(lib, &book)) {
fclose(fp);
return 0;
}
}
fclose(fp);
return 1;
}
⚠️
fscanf 的 %[^|] 格式读取直到 | 的字符串。%13[^|] 限制最多读 13 个字符,防止溢出。这是格式化读写的安全写法。
8. main.c
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "library.h"
#include "storage.h"
#define DATA_FILE "library.dat"
static void input_string(const char *prompt, char *buf, int size) {
printf("%s", prompt);
fflush(stdout);
if (fgets(buf, size, stdin) == NULL) {
buf[0] = '\0';
return;
}
buf[strcspn(buf, "\n")] = '\0';
}
static void cmd_add(Library *lib) {
Book book;
input_string("请输入ISBN: ", book.isbn, sizeof(book.isbn));
input_string("请输入书名: ", book.title, sizeof(book.title));
input_string("请输入作者: ", book.author, sizeof(book.author));
char price_str[32];
input_string("请输入价格: ", price_str, sizeof(price_str));
book.price = atof(price_str);
if (library_find_by_isbn(lib, book.isbn) != NULL) {
printf("错误:ISBN %s 已存在\n", book.isbn);
return;
}
if (library_add(lib, &book)) {
printf("添加成功!\n");
} else {
printf("添加失败:内存不足\n");
}
}
static void cmd_remove(Library *lib) {
char isbn[14];
input_string("请输入要删除的ISBN: ", isbn, sizeof(isbn));
if (library_remove(lib, isbn)) {
printf("删除成功!\n");
} else {
printf("未找到该ISBN的图书\n");
}
}
static void cmd_find(Library *lib) {
printf("1. 按ISBN查找 2. 按书名查找\n");
char choice[8];
input_string("请选择: ", choice, sizeof(choice));
if (strcmp(choice, "1") == 0) {
char isbn[14];
input_string("请输入ISBN: ", isbn, sizeof(isbn));
Book *book = library_find_by_isbn(lib, isbn);
if (book != NULL) {
printf("ISBN: %s\n", book->isbn);
printf("书名: %s\n", book->title);
printf("作者: %s\n", book->author);
printf("价格: %.2f\n", book->price);
} else {
printf("未找到\n");
}
} else if (strcmp(choice, "2") == 0) {
char keyword[128];
input_string("请输入关键词: ", keyword, sizeof(keyword));
Book *results[100];
int result_count = 0;
library_find_by_title(lib, keyword, results, &result_count);
if (result_count == 0) {
printf("未找到包含\"%s\"的图书\n", keyword);
} else {
for (int i = 0; i < result_count; i++) {
printf("%-14s %-30s %.2f\n",
results[i]->isbn, results[i]->title, results[i]->price);
}
printf("找到 %d 本\n", result_count);
}
}
}
static void cmd_update(Library *lib) {
char isbn[14];
input_string("请输入要修改的ISBN: ", isbn, sizeof(isbn));
Book *existing = library_find_by_isbn(lib, isbn);
if (existing == NULL) {
printf("未找到该ISBN的图书\n");
return;
}
Book new_info = *existing;
printf("当前书名: %s(直接回车保持不变)\n", existing->title);
input_string("新书名: ", new_info.title, sizeof(new_info.title));
if (new_info.title[0] == '\0') {
strcpy(new_info.title, existing->title);
}
printf("当前作者: %s(直接回车保持不变)\n", existing->author);
input_string("新作者: ", new_info.author, sizeof(new_info.author));
if (new_info.author[0] == '\0') {
strcpy(new_info.author, existing->author);
}
char price_str[32];
printf("当前价格: %.2f(直接回车保持不变)\n", existing->price);
input_string("新价格: ", price_str, sizeof(price_str));
if (price_str[0] != '\0') {
new_info.price = atof(price_str);
} else {
new_info.price = existing->price;
}
strcpy(new_info.isbn, isbn);
if (library_update(lib, isbn, &new_info)) {
printf("修改成功!\n");
}
}
static void show_menu(void) {
printf("\n===== 图书管理系统 =====\n");
printf("1. 添加图书\n");
printf("2. 删除图书\n");
printf("3. 查询图书\n");
printf("4. 修改图书\n");
printf("5. 显示所有图书\n");
printf("6. 保存数据\n");
printf("0. 退出\n");
printf("========================\n");
}
int main(void) {
Library lib;
library_init(&lib);
if (!storage_load(&lib, DATA_FILE)) {
printf("警告:加载数据失败,从空库开始\n");
}
char choice[8];
while (1) {
show_menu();
input_string("请选择操作: ", choice, sizeof(choice));
if (strcmp(choice, "1") == 0) {
cmd_add(&lib);
} else if (strcmp(choice, "2") == 0) {
cmd_remove(&lib);
} else if (strcmp(choice, "3") == 0) {
cmd_find(&lib);
} else if (strcmp(choice, "4") == 0) {
cmd_update(&lib);
} else if (strcmp(choice, "5") == 0) {
library_list(&lib);
} else if (strcmp(choice, "6") == 0) {
if (storage_save(&lib, DATA_FILE)) {
printf("保存成功!\n");
} else {
printf("保存失败!\n");
}
} else if (strcmp(choice, "0") == 0) {
printf("是否保存数据?(y/n): ");
char confirm[8];
if (fgets(confirm, sizeof(confirm), stdin) != NULL) {
if (confirm[0] == 'y' || confirm[0] == 'Y') {
storage_save(&lib, DATA_FILE);
printf("已保存\n");
}
}
break;
} else {
printf("无效选择\n");
}
}
library_free(&lib);
return 0;
}
9. Makefile
MAKEFILE
CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -O2
SRCS = main.c library.c storage.c
OBJS = $(SRCS:.c=.o)
TARGET = bookmanager
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
main.o: main.c library.h storage.h
library.o: library.c library.h
storage.o: storage.c storage.h library.h
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: clean
编译和运行:
BASH
make
./bookmanager
10. 运行效果
TEXT
📖 仅展示
===== 图书管理系统 =====
1. 添加图书
2. 删除图书
3. 查询图书
4. 修改图书
5. 显示所有图书
6. 保存数据
0. 退出
========================
请选择操作: 1
请输入ISBN: 9787115279460
请输入书名: C Primer Plus
请输入作者: Stephen Prata
请输入价格: 89.00
添加成功!
请选择操作: 5
ISBN 书名 作者 价格
--------------------------------------------------------------
9787115279460 C Primer Plus Stephen Prata 89.00
共 1 本图书
11. 项目要点解析
(1) 动态数组扩容
ensure_capacity 采用翻倍扩容策略。每次 count 达到 capacity 时,容量翻倍。这样 n 次插入的总扩容次数是 O(log n),每次插入均摊 O(1)。
(2) 文件格式选择
存储格式用 | 分隔的文本格式,而不是二进制格式。好处是:
- 人类可读,方便调试
- 不受平台字节序和对齐方式影响
- 可以用文本编辑器直接修改数据
(3) 模块划分
library 和 storage 分离——核心逻辑不依赖具体存储方式。将来换成数据库存储,只需修改 storage.c,核心代码不需要改。
(4) 输入安全
input_string 用 fgets 读取输入,自动限制长度,再用 strcspn 去掉换行符。这是比 scanf 更安全的输入方式。
▶ 示例
C
#include <stdio.h>
#include <string.h>
typedef struct {
char title[64];
char author[32];
int id;
} Book;
int main(void) {
Book b = {"C Primer Plus", "Stephen Prata", 1};
printf("编号:%d 书名:%s 作者:%s\n", b.id, b.title, b.author);
return 0;
}
TEXT
📖 仅展示
编号:1 书名:C Primer Plus 作者:Stephen Prata
❓ 常见问题
Q 为什么退出时不自动保存?
A 用户可能做了误操作,退出时询问是否保存给用户一个反悔的机会。自动保存可能覆盖掉之前的数据。
Q 动态数组删除元素为什么要移动后面的元素?
A 因为数组在内存中连续存储,删除中间的元素后必须把后面的元素前移填补空隙,否则查找和遍历会出问题。
Q 这个项目能支持多少本图书?
A 理论上受内存限制。每本图书约 210 字节,1GB 内存可存约 500 万本。实际瓶颈在搜索效率——线性查找 O(n) 在数据量大时会很慢,应改用哈希表或二叉搜索树。
Q Makefile 中 %.o 那行是什么意思?
A 这是模式规则,表示所有 .c 文件用同样的命令编译成 .o 文件。
$< 是第一个依赖(.c文件),$@ 是目标(.o文件)。📖 小节
- 需求分析是项目的起点,明确功能后再动手写代码
- 数据结构设计决定程序架构,动态数组是常用的可变长容器
- 模块化编程:头文件声明接口,源文件实现细节
- 文件持久化让数据在程序退出后不丢失
- Makefile 自动化构建,避免手动输入冗长的编译命令
📝 作业
- 为图书管理系统增加"按作者查找"功能
- 增加排序功能,可以按书名、价格或ISBN排序显示
- 改进存储模块,改用二进制文件格式存储,对比文本格式的优劣