練習:独自型の総合

必要な知識は十分身につきました。いよいよ何かを作る番です。料理と同じで、レシピを読むだけでは不十分で、実際にキッチンに立つ必要があります。構造体、動的メモリ、ファイル入出力を組み合わせて動くプログラムを作りましょう。

プロジェクト1:学生成績管理システム

要件

データ構造の設計

C
typedef struct {
    char name[32];
    char id[16];
    int chinese;
    int math;
    int english;
} Student;

typedef struct {
    Student *data;
    int count;
    int capacity;
} StudentList;

StudentList は動的配列で学生を管理します。初期容量は4で、満杯になると拡張します。

コア実装

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

typedef struct {
    char name[32];
    char id[16];
    int chinese;
    int math;
    int english;
} Student;

typedef struct {
    Student *data;
    int count;
    int capacity;
} StudentList;

void list_init(StudentList *list) {
    list->capacity = 4;
    list->count = 0;
    list->data = (Student *)malloc(sizeof(Student) * list->capacity);
}

void list_free(StudentList *list) {
    free(list->data);
    list->data = NULL;
    list->count = 0;
    list->capacity = 0;
}

void list_expand(StudentList *list) {
    if (list->count < list->capacity) return;
    list->capacity *= 2;
    Student *tmp = (Student *)realloc(list->data, sizeof(Student) * list->capacity);
    if (tmp) list->data = tmp;
}

void list_add(StudentList *list, const Student *s) {
    list_expand(list);
    list->data[list->count++] = *s;
}

int total_score(const Student *s) {
    return s->chinese + s->math + s->english;
}

double avg_score(const Student *s) {
    return total_score(s) / 3.0;
}

Student *find_by_id(StudentList *list, const char *id) {
    for (int i = 0; i < list->count; i++) {
        if (strcmp(list->data[i].id, id) == 0) {
            return &list->data[i];
        }
    }
    return NULL;
}

int cmp_by_total(const void *a, const void *b) {
    int ta = total_score((const Student *)a);
    int tb = total_score((const Student *)b);
    return tb - ta;
}

void sort_by_total(StudentList *list) {
    qsort(list->data, list->count, sizeof(Student), cmp_by_total);
}

void print_student(const Student *s) {
    printf("%-6s %-10s Chinese:%-3d Math:%-3d English:%-3d Total:%-4d Avg:%.1f\n",
           s->id, s->name, s->chinese, s->math, s->english,
           total_score(s), avg_score(s));
}

void print_all(const StudentList *list) {
    printf("ID     Name        Chinese Math English Total Avg\n");
    printf("----------------------------------------------------------\n");
    for (int i = 0; i < list->count; i++) {
        print_student(&list->data[i]);
    }
}

int save_to_file(const StudentList *list, const char *filename) {
    FILE *fp = fopen(filename, "w");
    if (!fp) return -1;
    fprintf(fp, "%d\n", list->count);
    for (int i = 0; i < list->count; i++) {
        Student *s = &list->data[i];
        fprintf(fp, "%s %s %d %d %d\n", s->id, s->name,
                s->chinese, s->math, s->english);
    }
    fclose(fp);
    return 0;
}

int load_from_file(StudentList *list, const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) return -1;
    int n;
    fscanf(fp, "%d", &n);
    for (int i = 0; i < n; i++) {
        Student s;
        fscanf(fp, "%s %s %d %d %d", s.id, s.name,
               &s.chinese, &s.math, &s.english);
        list_add(list, &s);
    }
    fclose(fp);
    return 0;
}

C
int main(void) {
    StudentList list;
    list_init(&list);

    Student s1 = {"001", "Zhang", 85, 92, 78};
    Student s2 = {"002", "Li", 90, 88, 95};
    Student s3 = {"003", "Wang", 72, 65, 80};

    list_add(&list, &s1);
    list_add(&list, &s2);
    list_add(&list, &s3);

    printf("--- All Students ---\n");
    print_all(&list);

    sort_by_total(&list);
    printf("\n--- Sorted by Total ---\n");
    print_all(&list);

    Student *found = find_by_id(&list, "001");
    if (found) {
        printf("\nFound 001: %s Total=%d\n", found->name, total_score(found));
    }

    save_to_file(&list, "students.dat");
    printf("\nSaved to students.dat\n");

    list_free(&list);
    return 0;
}
▶ 試してみよう
TEXT
--- All Students ---
ID     Name        Chinese Math English Total Avg
----------------------------------------------------------
001    Zhang       Chinese:85  Math:92  English:78  Total:255  Avg:85.0
002    Li          Chinese:90  Math:88  English:95  Total:273  Avg:91.0
003    Wang        Chinese:72  Math:65  English:80  Total:217  Avg:72.3

--- Sorted by Total ---
ID     Name        Chinese Math English Total Avg
----------------------------------------------------------
002    Li          Chinese:90  Math:88  English:95  Total:273  Avg:91.0
001    Zhang       Chinese:85  Math:92  English:78  Total:255  Avg:85.0
003    Wang        Chinese:72  Math:65  English:80  Total:217  Avg:72.3

Found 001: Zhang Total=255

Saved to students.dat

プロジェクト2:連絡先管理

要件

データ構造の設計

C
typedef struct {
    char name[32];
    char phone[16];
    char email[40];
    char group[16];
} Contact;

typedef struct {
    Contact *data;
    int count;
    int capacity;
} ContactList;

コア実装

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

typedef struct {
    char name[32];
    char phone[16];
    char email[40];
    char group[16];
} Contact;

typedef struct {
    Contact *data;
    int count;
    int capacity;
} ContactList;

void clist_init(ContactList *list) {
    list->capacity = 4;
    list->count = 0;
    list->data = (Contact *)malloc(sizeof(Contact) * list->capacity);
}

void clist_free(ContactList *list) {
    free(list->data);
    list->data = NULL;
    list->count = 0;
    list->capacity = 0;
}

void clist_expand(ContactList *list) {
    if (list->count < list->capacity) return;
    list->capacity *= 2;
    Contact *tmp = (Contact *)realloc(list->data, sizeof(Contact) * list->capacity);
    if (tmp) list->data = tmp;
}

void clist_add(ContactList *list, const Contact *c) {
    clist_expand(list);
    list->data[list->count++] = *c;
}

int clist_remove(ContactList *list, const char *name) {
    for (int i = 0; i < list->count; i++) {
        if (strcmp(list->data[i].name, name) == 0) {
            list->data[i] = list->data[list->count - 1];
            list->count--;
            return 0;
        }
    }
    return -1;
}

Contact *clist_find(ContactList *list, const char *name) {
    for (int i = 0; i < list->count; i++) {
        if (strcmp(list->data[i].name, name) == 0) {
            return &list->data[i];
        }
    }
    return NULL;
}

void clist_filter_by_group(const ContactList *list, const char *group) {
    printf("--- Group: %s ---\n", group);
    for (int i = 0; i < list->count; i++) {
        if (strcmp(list->data[i].group, group) == 0) {
            printf("%s  %s  %s\n", list->data[i].name,
                   list->data[i].phone, list->data[i].email);
        }
    }
}

void print_contact(const Contact *c) {
    printf("%-8s %-14s %-24s %s\n", c->name, c->phone, c->email, c->group);
}

void print_all_contacts(const ContactList *list) {
    printf("%-8s %-14s %-24s %s\n", "Name", "Phone", "Email", "Group");
    printf("---------------------------------------------------------\n");
    for (int i = 0; i < list->count; i++) {
        print_contact(&list->data[i]);
    }
}

int save_contacts(const ContactList *list, const char *filename) {
    FILE *fp = fopen(filename, "w");
    if (!fp) return -1;
    fprintf(fp, "%d\n", list->count);
    for (int i = 0; i < list->count; i++) {
        Contact *c = &list->data[i];
        fprintf(fp, "%s %s %s %s\n", c->name, c->phone, c->email, c->group);
    }
    fclose(fp);
    return 0;
}

int load_contacts(ContactList *list, const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) return -1;
    int n;
    fscanf(fp, "%d", &n);
    for (int i = 0; i < n; i++) {
        Contact c;
        fscanf(fp, "%s %s %s %s", c.name, c.phone, c.email, c.group);
        clist_add(list, &c);
    }
    fclose(fp);
    return 0;
}

C
int main(void) {
    ContactList list;
    clist_init(&list);

    Contact c1 = {"Zhang", "13800001111", "zhangsan@mail.com", "Coworker"};
    Contact c2 = {"Li", "13900002222", "lisi@mail.com", "Friend"};
    Contact c3 = {"Wang", "15000003333", "wangwu@mail.com", "Coworker"};
    Contact c4 = {"Zhao", "18600004444", "zhaoliu@mail.com", "Family"};

    clist_add(&list, &c1);
    clist_add(&list, &c2);
    clist_add(&list, &c3);
    clist_add(&list, &c4);

    printf("--- All Contacts ---\n");
    print_all_contacts(&list);

    clist_filter_by_group(&list, "Coworker");

    Contact *found = clist_find(&list, "Li");
    if (found) {
        printf("\nFound Li: %s %s\n", found->phone, found->email);
    }

    clist_remove(&list, "Wang");
    printf("\n--- After Deleting Wang ---\n");
    print_all_contacts(&list);

    save_contacts(&list, "contacts.dat");
    printf("\nSaved to contacts.dat\n");

    clist_free(&list);
    return 0;
}
▶ 試してみよう
TEXT
--- All Contacts ---
Name     Phone           Email                    Group
---------------------------------------------------------
Zhang    13800001111     zhangsan@mail.com        Coworker
Li       13900002222     lisi@mail.com            Friend
Wang     15000003333     wangwu@mail.com          Coworker
Zhao     18600004444     zhaoliu@mail.com         Family

--- Group: Coworker ---
Zhang  13800001111  zhangsan@mail.com
Wang  15000003333  wangwu@mail.com

Found Li: 13900002222 lisi@mail.com

--- After Deleting Wang ---
Name     Phone           Email                    Group
---------------------------------------------------------
Zhang    13800001111     zhangsan@mail.com        Coworker
Li       13900002222     lisi@mail.com            Friend
Zhao     18600004444     zhaoliu@mail.com         Family

Saved to contacts.dat

主要パターンの復習

動的配列パターン

両プロジェクトで同じ動的配列パターンを使っています:

  1. 構造体に データポインタ + 要素数 + 容量 を持たせる
  2. init で初期メモリを割り当てる
  3. add で容量を確認し、満杯なら realloc で2倍に拡張する
  4. free でメモリを解放しNULLに設定する

これはC言語で動的コレクションを実装する最も一般的な方法です。一度習得すれば、どんな場面でも再利用できます。

ファイル入出力パターン

C
fprintf(fp, "%d\n", count);
for (int i = 0; i < count; i++) {
    fprintf(fp, "field1 field2 ...\n", ...);
}

読み込み時はまず要素数を読み、次にレコードをループで読み込みます。%s は空白で止まるため、フィールドに空白が含まれないようにするか、fscanf の固定フォーマットを使ってください。

削除のテクニック

配列の中間要素を削除する際、最後の要素で上書きしてから count-- します。これにより多くの要素の移動を避けられますが、順序は保持されません。順序が必要な場合は memmove で後続要素を前にシフトしてください。

❓ よくある質問

Q 容量を1ずつ増やすのではなく2倍にするのはなぜですか?
A 2倍拡張は償却O(1)の戦略であり、1ずつ増やす場合よりコピーの総数がはるかに少なくなります。これはアルゴリズム解析の古典的結果であり、C++のvectorも同じ戦略を使っています。
Q ファイルはバイナリとテキストのどちらで保存すべきですか?
A テキスト形式は読みやすく、手動で編集でき、クロスプラットフォームです。バイナリはよりコンパクトで読み書きが速いですが、人間が読めません。学習プロジェクトではテキスト形式の方が直感的です。
Q 末尾と入れ替える削除で順序を保持するにはどうすればよいですか?
A memmove で要素を前にシフトしてください:memmove(&data[i], &data[i+1], (count-i-1)*sizeof(T))。または連結リスト構造に切り替えてください。
Q 名前に空白が含まれる場合はどうすればよいですか?
A scanf/fscanf%s は空白で止まります。fgets で行全体を読み取るか、空白の代わりにアンダースコアを使うか、%31[^\n] のような固定幅フォーマットを使ってください。

📖 まとめ

📝 練習問題

  1. 学生成績管理システムに「成績編集」機能を追加してください。学籍番号と新しい成績を入力して対応する学生レコードを更新します
  2. 連絡先マネージャーに「電話番号による検索」機能を追加し、保存時に名前順でソートされるように変更してください(挿入時に順序を維持)
  3. 両プロジェクトを複数ファイル構造に分割してください。各プロジェクトに少なくとも .h 宣言ファイルと .c 実装ファイルを用意し、対応するMakefileを作成してください
100%