最終プロジェクト:図書管理システム
家を建てるのはレンガの積み方を学ぶだけではありません——今こそすべての技術を組み合わせて、基礎から屋根まで完全な建物を建てる時です。
要件分析
コマンドラインベースの図書管理システムを構築します。機能は以下の通りです:
- 図書の追加(書名、著者、ISBN、価格)
- 図書の削除(ISBNで指定)
- 図書情報の更新
- 図書の検索(書名またはISBNで)
- 全図書の一覧表示
- データのファイル保存(永続化)
- ファイルからのデータ読み込み
データ構造設計
図書情報は構造体で表現し、全図書は動的配列で管理します:
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に達すると自動的に拡張します。
プロジェクトのファイル構成
bookmanager/
├── Makefile
├── main.c
├── library.h
├── library.c
├── storage.h
└── storage.c
library.h/library.c:図書管理のコアロジック(CRUD)storage.h/storage.c:ファイル入出力main.c:メインプログラムのエントリポイントとユーザーインターフェースMakefile:ビルドスクリプト
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
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)の挿入効率を保証します。
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
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文字までに制限し、オーバーフローを防止します。これがフォーマット入出力の安全な使い方です。
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(Enterでそのまま)\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(Enterでそのまま)\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(Enterでそのまま)\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;
}
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
プログラムの実行
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冊
プロジェクトの要点
動的配列の拡張
ensure_capacityは倍増戦略を使います。countがcapacityに達するたびに容量が2倍になります。これはn回の挿入でO(log n)回の再確保しか発生せず、挿入ごとの償却コストはO(1)になります。
ファイルフォーマットの選択
保存フォーマットはバイナリではなく|区切りのテキストを使います。利点:
- 人間が読める、デバッグが容易
- プラットフォームのエンディアンやアライメントの影響を受けない
- テキストエディタで直接編集可能
モジュールの分離
libraryとstorageは疎結合です——コアロジックは具体的な保存方法に依存しません。後でデータベースに切り替える場合、storage.cを変更するだけで、コアコードの変更は不要です。
入力の安全性
input_stringはfgetsで入力を行い、自動的に長さを制限し、strcspnで改行を削除します。これはscanfより安全な入力方法です。
❓ よくある質問
Q なぜ終了時に自動保存しないのですか?
A ユーザーが誤った変更をしている可能性があるためです。保存するかどうかを尋ねることで、取り消す機会を与えます。自動保存は以前のデータを上書きしてしまう危険があります。
Q なぜ動的配列から要素を削除するときにシフトが必要なのですか?
A 配列はメモリ上に連続して格納されているため、中間の要素を削除した後、後続の要素を前へ詰める必要があります——そうしないと検索や走査が壊れます。
Q このプロジェクトは何冊までサポートできますか?
A 理論上はメモリによってのみ制限されます。1冊あたり約210バイトなので、1GBのメモリで約500万冊を保持できます。実用的なボトルネックは検索効率であり——O(n)の線形探索はデータが大きくなると遅くなるため、ハッシュテーブルや二分探索木が適しています。
Q Makefileの%.oの行はどういう意味ですか?
A パターンルールであり、すべての.cファイルが同じコマンドで.oファイルにコンパイルされることを意味します。
$<は最初の依存関係(.cファイル)、$@はターゲット(.oファイル)です。📖 まとめ
- 要件分析が出発点——コードを書く前に機能を定義する
- データ構造設計がプログラムアーキテクチャを決定、動的配列は一般的な可変長コンテナ
- モジュラープログラミング:ヘッダファイルはインターフェースを宣言、ソースファイルは詳細を実装
- ファイル永続化によりプログラム終了後もデータが残る
- Makefileがビルドを自動化し、長いコンパイルコマンドの手入力を不要にする
📝 練習問題
- 図書管理システムに「著者で検索」機能を追加してください
- 書名、価格、またはISBNでソートして表示する機能を追加してください
- 保存モジュールをバイナリファイルフォーマットに切り替え、テキストフォーマットとの長所と短所を比較してください



