最終プロジェクト:図書管理システム

家を建てるのはレンガの積み方を学ぶだけではありません——今こそすべての技術を組み合わせて、基礎から屋根まで完全な建物を建てる時です。

要件分析

コマンドラインベースの図書管理システムを構築します。機能は以下の通りです:

  1. 図書の追加(書名、著者、ISBN、価格)
  2. 図書の削除(ISBNで指定)
  3. 図書情報の更新
  4. 図書の検索(書名またはISBNで)
  5. 全図書の一覧表示
  6. データのファイル保存(永続化)
  7. ファイルからのデータ読み込み

データ構造設計

図書情報は構造体で表現し、全図書は動的配列で管理します:

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は確保済みの容量です。countcapacityに達すると自動的に拡張します。

プロジェクトのファイル構成

bookmanager/
├── Makefile
├── main.c
├── library.h
├── library.c
├── storage.h
└── storage.c

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は倍増戦略を使います。countcapacityに達するたびに容量が2倍になります。これはn回の挿入でO(log n)回の再確保しか発生せず、挿入ごとの償却コストはO(1)になります。

ファイルフォーマットの選択

保存フォーマットはバイナリではなく|区切りのテキストを使います。利点:

モジュールの分離

librarystorageは疎結合です——コアロジックは具体的な保存方法に依存しません。後でデータベースに切り替える場合、storage.cを変更するだけで、コアコードの変更は不要です。

入力の安全性

input_stringfgetsで入力を行い、自動的に長さを制限し、strcspnで改行を削除します。これはscanfより安全な入力方法です。

❓ よくある質問

Q なぜ終了時に自動保存しないのですか?
A ユーザーが誤った変更をしている可能性があるためです。保存するかどうかを尋ねることで、取り消す機会を与えます。自動保存は以前のデータを上書きしてしまう危険があります。
Q なぜ動的配列から要素を削除するときにシフトが必要なのですか?
A 配列はメモリ上に連続して格納されているため、中間の要素を削除した後、後続の要素を前へ詰める必要があります——そうしないと検索や走査が壊れます。
Q このプロジェクトは何冊までサポートできますか?
A 理論上はメモリによってのみ制限されます。1冊あたり約210バイトなので、1GBのメモリで約500万冊を保持できます。実用的なボトルネックは検索効率であり——O(n)の線形探索はデータが大きくなると遅くなるため、ハッシュテーブルや二分探索木が適しています。
Q Makefileの%.oの行はどういう意味ですか?
A パターンルールであり、すべての.cファイルが同じコマンドで.oファイルにコンパイルされることを意味します。$<は最初の依存関係(.cファイル)、$@はターゲット(.oファイル)です。

📖 まとめ

📝 練習問題

  1. 図書管理システムに「著者で検索」機能を追加してください
  2. 書名、価格、またはISBNでソートして表示する機能を追加してください
  3. 保存モジュールをバイナリファイルフォーマットに切り替え、テキストフォーマットとの長所と短所を比較してください
100%