[C言語のはじめ方] Part32: 簡易データベースの実装(構造体 + ファイル)

C言語

C言語でデータを永続化する方法を学ぼう!

これまでのステップで、構造体を使って複雑なデータをまとめたり、ファイル入出力でデータを読み書きする方法を学びましたね。今回は、これらの知識を組み合わせて、簡単な「データベース」を作成する方法を学びます。データベースと言っても、本格的なものではなく、構造体のデータをファイルに保存し、後で読み込んだり検索したりできるようにするものです。これにより、プログラムを終了してもデータが消えずに残るようになります 🎉。

このステップでは、連絡先リストを管理する簡単なデータベースを例に、データの追加、表示、検索といった基本的な機能を実装していきます。

1. データベースの設計:構造体を定義する 📝

まず、データベースにどのような情報を保存したいかを決め、それに対応する構造体を定義します。今回は、簡単な連絡先リストを作るので、名前、電話番号、メールアドレスを保存することにしましょう。

#include <stdio.h>

// 連絡先情報を格納する構造体
typedef struct {
    char name[50];      // 名前 (最大49文字 + ヌル文字)
    char phone[20];     // 電話番号 (最大19文字 + ヌル文字)
    char email[50];     // メールアドレス (最大49文字 + ヌル文字)
} Contact;

typedef struct { ... } Contact; という書き方で、struct Contact の代わりに Contact という名前でこの構造体型を使えるようにしています。各メンバは文字配列(文字列)として定義しています。

2. ファイル操作の基本:バイナリモードでの読み書き 📁

構造体のようなデータをファイルに保存する場合、テキスト形式ではなくバイナリ形式で保存するのが一般的です。バイナリ形式は、メモリ上のデータ表現をそのままファイルに書き込むため、構造体全体の読み書きが効率的に行えます。

ファイル操作には、これまで学んだ fopen, fclose に加え、バイナリデータの読み書きに使われる fwritefread 関数を使用します。

  • fopen(ファイル名, モード): ファイルを開きます。バイナリモードで書き込む場合は "wb" (Write Binary)、追記する場合は "ab" (Append Binary)、読み込む場合は "rb" (Read Binary) を指定します。
  • fwrite(データポインタ, 1要素のサイズ, 要素数, ファイルポインタ): バイナリデータをファイルに書き込みます。
  • fread(データポインタ, 1要素のサイズ, 要素数, ファイルポインタ): ファイルからバイナリデータを読み込みます。
  • fclose(ファイルポインタ): ファイルを閉じます。
注意: ファイル操作を行う際は、必ずエラーチェック(fopen の戻り値が NULL でないかなど)を行い、最後に fclose でファイルを閉じることを忘れないでください。

3. データの追加機能の実装 ➕

ユーザーから入力された連絡先情報を構造体に格納し、それをファイルに追加する関数を作成してみましょう。ファイルは追記モード ("ab") で開くことで、既存のデータを消さずに末尾に新しいデータを追加できます。

#include <stdio.h>
#include <stdlib.h> // exit関数用

// 上で定義したContact構造体があるとする
// typedef struct { ... } Contact;

// 連絡先をファイルに追加する関数
void addContact(const char* filename) {
    Contact newContact;
    FILE *fp;

    // ユーザーに入力を促す
    printf("新しい連絡先の情報を入力してください。\n");
    printf("名前: ");
    scanf("%49s", newContact.name); // バッファオーバーフロー対策
    printf("電話番号: ");
    scanf("%19s", newContact.phone);
    printf("メールアドレス: ");
    scanf("%49s", newContact.email);

    // ファイルを追記バイナリモードで開く
    fp = fopen(filename, "ab");
    if (fp == NULL) {
        perror("ファイルを開けませんでした");
        exit(EXIT_FAILURE); // エラー発生時はプログラム終了
    }

    // 構造体のデータをファイルに書き込む
    // fwrite(書き込むデータのアドレス, 1要素のサイズ, 要素数, ファイルポインタ)
    size_t written = fwrite(&newContact, sizeof(Contact), 1, fp);
    if (written < 1) {
        perror("ファイルへの書き込みに失敗しました");
        // 必要に応じて部分的に書き込まれた場合の処理を追加
    } else {
        printf("連絡先を追加しました。\n");
    }

    // ファイルを閉じる
    fclose(fp);
}

fwrite 関数の第1引数には書き込むデータのアドレス (&newContact)、第2引数には1要素のサイズ (sizeof(Contact))、第3引数には要素数 (今回は1つなので 1)、第4引数にはファイルポインタ (fp) を指定します。戻り値は実際に書き込まれた要素数なので、エラーチェックに利用できます。

💡 ヒント: scanf で文字列を読み込む際は、%s ではなく %49s のように最大文字数を指定することで、配列のサイズを超える入力によるバッファオーバーフローを簡易的に防ぐことができます。

4. データの表示機能の実装 📄

ファイルに保存されている全ての連絡先データを読み込み、画面に表示する関数を作成しましょう。ファイルは読み込みバイナリモード ("rb") で開きます。fread を使って、ファイル終端に達するまで構造体データを1つずつ読み込みます。

#include <stdio.h>
#include <stdlib.h>

// Contact構造体定義は省略

// 全ての連絡先をファイルから読み込んで表示する関数
void displayContacts(const char* filename) {
    Contact currentContact;
    FILE *fp;
    int count = 0;

    // ファイルを読み込みバイナリモードで開く
    fp = fopen(filename, "rb");
    if (fp == NULL) {
        // ファイルが存在しない場合などはエラーではないかもしれない
        printf("連絡先データが見つかりません。\n");
        return; // 関数を抜ける
    }

    printf("\n--- 連絡先一覧 ---\n");
    // fread(読み込みデータの格納先アドレス, 1要素のサイズ, 要素数, ファイルポインタ)
    // 戻り値が指定した要素数 (ここでは1) であれば読み込み成功
    while (fread(&currentContact, sizeof(Contact), 1, fp) == 1) {
        count++;
        printf("--- [%d] ---\n", count);
        printf("名前: %s\n", currentContact.name);
        printf("電話番号: %s\n", currentContact.phone);
        printf("メールアドレス: %s\n\n", currentContact.email);
    }

    if (ferror(fp)) { // 読み込み中にエラーが発生したかチェック
      perror("ファイル読み込み中にエラーが発生しました");
    } else if (count == 0) {
        printf("連絡先は登録されていません。\n");
    }

    printf("------------------\n");

    // ファイルを閉じる
    fclose(fp);
}

fread は、指定した要素数を読み込めた場合にその数を返します。ファイル終端に達したり、エラーが発生したりすると、指定した要素数未満の値(通常は0)を返します。while ループと組み合わせることで、ファイル内の全てのレコードを処理できます。ループ終了後、ferror(fp) で読み込みエラーがなかったか確認するとより安全です。

5. データの検索機能の実装 🔍

特定の条件(例:名前に特定の文字列を含む)に一致する連絡先をファイルから探し出して表示する関数を実装しましょう。基本的な流れは表示機能と似ていますが、読み込んだデータが条件に合致するかをチェックする処理が加わります。文字列の比較には strcmp 関数(完全一致)や strstr 関数(部分一致)などが使えます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h> // strcmp関数用

// Contact構造体定義は省略

// 名前で連絡先を検索する関数 (完全一致)
void searchContactByName(const char* filename, const char* searchName) {
    Contact currentContact;
    FILE *fp;
    int found = 0;

    fp = fopen(filename, "rb");
    if (fp == NULL) {
        printf("連絡先データが見つかりません。\n");
        return;
    }

    printf("\n--- 「%s」の検索結果 ---\n", searchName);
    while (fread(&currentContact, sizeof(Contact), 1, fp) == 1) {
        // strcmpは文字列が一致すれば0を返す
        if (strcmp(currentContact.name, searchName) == 0) {
            found++;
            printf("名前: %s\n", currentContact.name);
            printf("電話番号: %s\n", currentContact.phone);
            printf("メールアドレス: %s\n\n", currentContact.email);
            // 完全一致なら通常1件だけだが、複数件の可能性も考慮する場合はbreakしない
        }
    }

    if (ferror(fp)) {
      perror("ファイル読み込み中にエラーが発生しました");
    } else if (found == 0) {
        printf("該当する連絡先は見つかりませんでした。\n");
    }

    printf("-----------------------\n");
    fclose(fp);
}

この例では strcmp を使って名前が完全に一致する連絡先を探しています。部分一致で検索したい場合は、strstr(currentContact.name, searchName) != NULL のような条件式を使います。

6. 発展:データの更新と削除 🔄🗑️

データの更新や削除は、少し複雑になります。なぜなら、通常、ファイル内の特定のレコードだけを直接書き換えたり削除したりするのは簡単ではないからです。一般的なアプローチとしては、以下のような方法があります。

  • 更新:
    1. 元のファイルを読み込みモード ("rb") で開きます。
    2. 新しい一時ファイル(例: “temp.dat”)を書き込みモード ("wb") で開きます。
    3. 元のファイルを1レコードずつ読み込みます。
    4. 更新対象のレコードが見つかったら、更新後のデータを一時ファイルに書き込みます。
    5. 更新対象でないレコードは、そのまま一時ファイルに書き込みます。
    6. 元のファイルを閉じ、一時ファイルを閉じます。
    7. 元のファイルを削除します (remove()関数)。
    8. 一時ファイルの名前を元のファイル名に変更します (rename()関数)。
  • 削除: 更新とほぼ同じ手順ですが、削除対象のレコードが見つかった場合に、それを一時ファイルに書き込まないようにします。

これらの処理は、ファイルI/Oが多くなり、エラーハンドリングもより重要になります。初学者の段階では、まずは追加・表示・検索の機能を確実に実装できるようになることを目指しましょう 💪。

7. まとめとサンプルコード 🚀

今回は、構造体とファイル操作を組み合わせて、簡易的なデータベースを作成する方法を学びました。

  • 構造体を使って管理したいデータを定義する。
  • ファイル操作(fopen, fclose, fwrite, fread)を使ってデータを永続化する。
  • バイナリモード ("rb", "wb", "ab") を使うと構造体データを効率的に扱える。
  • データの追加、表示、検索といった基本的なデータベース操作を実装できる。
  • 更新や削除は、一時ファイルを利用するなどの工夫が必要。
  • エラー処理は常に重要!

この技術は、設定ファイル、ログファイル、簡単なアプリケーションのデータ保存など、様々な場面で応用できます。

以下に、ここまでの機能をまとめた簡単なサンプルコードを示します。(エラー処理などは簡略化しています)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FILENAME "contacts.dat" // データファイル名

// 連絡先情報を格納する構造体
typedef struct {
    char name[50];
    char phone[20];
    char email[50];
} Contact;

// --- 関数のプロトタイプ宣言 ---
void addContact();
void displayContacts();
void searchContact();
void printMenu();

int main() {
    int choice;

    while (1) {
        printMenu();
        printf("選択してください: ");
        if (scanf("%d", &choice) != 1) {
            // 不正な入力があった場合、入力バッファをクリア
            while (getchar() != '\n');
            printf("無効な入力です。数値を入力してください。\n");
            continue;
        }
        // 改行文字を読み飛ばす
        getchar();

        switch (choice) {
            case 1:
                addContact();
                break;
            case 2:
                displayContacts();
                break;
            case 3:
                searchContact();
                break;
            case 0:
                printf("プログラムを終了します。\n");
                exit(EXIT_SUCCESS);
            default:
                printf("無効な選択です。もう一度入力してください。\n");
        }
        printf("\n"); // メニュー前に改行
    }

    return 0; // 通常はここに到達しない
}

// メニュー表示
void printMenu() {
    printf("--- 連絡先管理 ---\n");
    printf("1. 連絡先を追加\n");
    printf("2. 連絡先を表示\n");
    printf("3. 連絡先を検索 (名前)\n");
    printf("0. 終了\n");
    printf("------------------\n");
}

// 連絡先を追加 (main関数から呼び出すため引数なし)
void addContact() {
    Contact newContact;
    FILE *fp;

    printf("--- 連絡先の追加 ---\n");
    printf("名前: ");
    // fgetsを使う方が安全だが、ここではscanfを使用
    if (scanf("%49s", newContact.name) != 1) { printf("入力エラー\n"); return; }
    printf("電話番号: ");
    if (scanf("%19s", newContact.phone) != 1) { printf("入力エラー\n"); return; }
    printf("メールアドレス: ");
    if (scanf("%49s", newContact.email) != 1) { printf("入力エラー\n"); return; }
    // 各scanfの後でバッファに残った改行文字をクリア
    while(getchar() != '\n');

    fp = fopen(FILENAME, "ab");
    if (fp == NULL) {
        perror("ファイルオープンエラー");
        return;
    }

    if (fwrite(&newContact, sizeof(Contact), 1, fp) < 1) {
        perror("書き込みエラー");
    } else {
        printf("連絡先を追加しました。\n");
    }
    fclose(fp);
}

// 連絡先を表示 (main関数から呼び出すため引数なし)
void displayContacts() {
    Contact currentContact;
    FILE *fp;
    int count = 0;

    fp = fopen(FILENAME, "rb");
    if (fp == NULL) {
        printf("連絡先データが存在しないか、開けません。\n");
        return;
    }

    printf("\n--- 連絡先一覧 ---\n");
    while (fread(&currentContact, sizeof(Contact), 1, fp) == 1) {
        count++;
        printf("--- [%d] ---\n", count);
        printf("  名前: %s\n", currentContact.name);
        printf("  電話番号: %s\n", currentContact.phone);
        printf("  メールアドレス: %s\n", currentContact.email);
    }
    if (ferror(fp)) {
        perror("読み込みエラー");
    } else if (count == 0) {
        printf("連絡先は登録されていません。\n");
    }
    printf("------------------\n");
    fclose(fp);
}

// 名前で連絡先を検索 (main関数から呼び出すため引数なし)
void searchContact() {
    char searchName[50];
    Contact currentContact;
    FILE *fp;
    int found = 0;

    printf("--- 連絡先の検索 ---\n");
    printf("検索する名前を入力してください: ");
    if (scanf("%49s", searchName) != 1) { printf("入力エラー\n"); return; }
    while(getchar() != '\n'); // バッファクリア

    fp = fopen(FILENAME, "rb");
    if (fp == NULL) {
        printf("連絡先データが存在しないか、開けません。\n");
        return;
    }

    printf("\n--- 検索結果 ---\n");
    while (fread(&currentContact, sizeof(Contact), 1, fp) == 1) {
        if (strcmp(currentContact.name, searchName) == 0) {
            found++;
            printf("  名前: %s\n", currentContact.name);
            printf("  電話番号: %s\n", currentContact.phone);
            printf("  メールアドレス: %s\n", currentContact.email);
            printf("---\n");
        }
    }
    if (ferror(fp)) {
        perror("読み込みエラー");
    } else if (found == 0) {
        printf("該当する連絡先は見つかりませんでした。\n");
    }
    printf("----------------\n");
    fclose(fp);
}

参考情報 🌐

これらの公式ドキュメントを参照することで、各関数の詳細な仕様や挙動、エラーハンドリングについてより深く理解できます。

コメント

タイトルとURLをコピーしました