コンテンツにスキップ

C言語/構造体

出典: フリー教科書『ウィキブックス(Wikibooks)』

構造体の基本[編集]

構造体とは何か[編集]

構造体の定義と必要性[編集]

C言語では、単一のデータ型(int、double、charなど)だけでなく、複数のデータ型を組み合わせて一つのまとまりとして扱うことができます。このようなデータの集合を「構造体」と呼びます。構造体は、異なるデータ型を持つ複数の変数を一つの単位として扱えるため、複雑なデータ構造を簡単に管理できるようになります。

基本的な使用例[編集]

例えば、学生の情報(名前、年齢、成績)を扱う場合、以下のように構造体を定義できます。

struct Student {
    char name[50];
    int age;
    double grade;
};

このように定義することで、一つの「Student」構造体変数に学生の名前、年齢、成績をまとめて格納できます。

構造体の宣言と定義[編集]

構造体の宣言方法[編集]

構造体を宣言するには、structキーワードを使います。構造体の名前を指定し、その中にメンバー変数を定義します。

struct StructName {
    dataType member1;
    dataType member2;
    // その他のメンバー
};

メンバー変数の定義[編集]

例えば、点の座標を表す構造体を定義するには、以下のようにします。

struct Point {
    int x;
    int y;
};

これにより、Pointという名前の構造体が定義され、xyの2つの整数メンバーを持ちます。

構造体の使用[編集]

構造体変数の定義と初期化[編集]

構造体変数の宣言[編集]

構造体を定義した後、その型を使って変数を宣言できます。

struct Point p1;

構造体変数の初期化方法[編集]

構造体変数を宣言すると同時に初期化することもできます。

struct Point p1 = {10.0, 20.0};
struct Point p2 = {.x = 10.0, .y = 20.0}; // 要素名を指定して初期化することも出来る
struct Point p3 = {.y = 20.0, .x = 10.0}; //   その場合は順序は問わない

構造体メンバーへのアクセス[編集]

ドット演算子(.)を使ったアクセス[編集]

構造体のメンバーにアクセスするには、ドット演算子(.)を使用します。

p1.x = 15;
printf("x: %f, y: %f", p1.x, p1.y);

これにより、p1xメンバーに15を代入し、p1xおよびyメンバーを表示できます。

構造体と配列[編集]

構造体配列の定義と使用[編集]

構造体配列の宣言と初期化[編集]

構造体を配列として定義することも可能です。

struct Point points[5];

構造体配列を初期化することもできます。

struct Point points[] = {{0.1, 0.9}, {1.1, 1.2}, {2.3, 2.7}};
struct Point points2[] = {{.x = 0.1, .y = 0.9}, {.y = 1.2, .x = 1.1}, {.x = 2.3, .y = 2.7}};

構造体配列の操作[編集]

配列の要素にアクセスして操作することができます。

points[0].x = 5.2;
printf("Point 0: x = %f, y = %f", points[0].x, points[0].y);

構造体を配列のメンバーとして使用[編集]

配列メンバーとしての構造体の使用例[編集]

構造体のメンバーとして配列を持つこともできます。

struct Polygon {
    struct Point vertices[5];
};

多次元構造体配列[編集]

多次元の構造体配列も使用できます。

struct Point grid[3][3];

ポインタと構造体[編集]

構造体へのポインタ[編集]

構造体ポインタの宣言と初期化[編集]

構造体へのポインタを使うことで、構造体を効率的に扱うことができます。

struct Point p = {10.0, 20.9};
struct Point *pPtr = &p;

アロー演算子(->)を使ったメンバーアクセス[編集]

構造体ポインタを使ってメンバーにアクセスする場合、アロー演算子(->)を使用します。

pPtr->x = 30.2;
printf("x: %f, y: %f", pPtr->x, pPtr->y);

構造体ポインタの活用[編集]

動的メモリ確保と構造体[編集]

動的に構造体のメモリを確保する場合は、malloc関数を使用します。

struct Point *pPtr = (struct Point *)malloc(sizeof(struct Point));
if (p == NULL) {
    perror("Failed to allocate memory");
    return 1;
}

pPtr->x = 10;
pPtr->y = 20;

構造体ポインタを使った関数引数の例[編集]

構造体ポインタを関数の引数として渡すことで、効率的なデータ操作が可能です。

void printPoint(struct Point *p) {
    printf("x: %f, y: %f", p->x, p->y);
}
printPoint(pPtr);

ネストした構造体[編集]

構造体内の構造体[編集]

ネスト構造体の定義と使用方法[編集]

構造体のメンバーとして他の構造体を定義することができます。

struct Rectangle {
    struct Point topLeft;
    struct Point bottomRight;
};

ネスト構造体の初期化[編集]

ネストした構造体も初期化可能です。

struct Rectangle rect = {{0.1, 0.9}, {10.1, 10.2}};
struct Rectangle rct2 = {.topLeft = {0.1, 0.9}, .bottomRight = {10.1, 10.2}};
struct Rectangle rct3 = {.topLeft.x = 0.1, .topLeft.y = 0.9, .bottomRight.x = 10.1, .bottomRight.y = 10.2};

ネスト構造体のアクセス[編集]

ネスト構造体メンバーへのアクセス方法[編集]

ネスト構造体のメンバーにアクセスする場合、ドット演算子を連続して使用します。

rect.topLeft.x = 5;
printf("Top-left x: %f", rect.topLeft.x);

ネスト構造体とポインタの併用[編集]

ネスト構造体をポインタで扱うこともできます。

struct Rectangle *rectPtr = ▭
rectPtr->topLeft.x = 5.5;
printf("Top-left x: %f", rectPtr->topLeft.x);

構造体と関数[編集]

関数への構造体の引数渡し[編集]

値渡しとポインタ渡し[編集]

構造体を関数に渡す際、値渡しとポインタ渡しのどちらも使用できます。値渡しの場合、関数内での変更は元の構造体に影響を与えません。

void modifyPoint(struct Point p) {
    p.x = 100;
}

ポインタ渡しの場合、関数内での変更は元の構造体に反映されます。

void modifyPointRef(struct Point *p) {
    if (p == NULL) {
        return;
    }
    p->x = 100;
}

構造体を返す関数[編集]

関数から構造体を返すことも可能です。

struct Point createPoint(int x, int y) {
    struct Point pt = (struct Point){x, y};
    return pt;
}

構造体を使ったプログラム設計[編集]

構造体を使ったモジュール設計[編集]

構造体を使うことで、データのカプセル化とモジュール設計が容易になります。例えば、複数の関数が共通して使用するデータを一つの構造体としてまとめることで、コードの可読性と保守性が向上します。

struct Account {
    int accountNumber;
    double balance;
};

void deposit(struct Account *acc, double amount) {
    acc->balance += amount;
}

double withdraw(struct Account *acc, double amount) {
    if (acc->balance >= amount) {
        acc->balance -= amount;
        return amount;
    } else {
        printf("Insufficient balance\n");
        return 0.0;
    }
}

int main() {
    struct Account myAccount = {1001, 500.0};

    deposit(&myAccount, 200.0);
    printf("Current balance: %.2f\n", myAccount.balance);

    double withdrawnAmount = withdraw(&myAccount, 100.0);
    if (withdrawnAmount > 0.0) {
        printf("Withdrawn amount: %.2f\n", withdrawnAmount);
    }
    printf("Current balance: %.2f\n", myAccount.balance);

    return 0;
}

この例では、Account構造体を定義し、それに対して預金(deposit)と引き出し(withdraw)の関数を定義しています。main関数では、myAccountという名前の構造体変数を作成し、預金と引き出しの操作を行っています。

構造体とファイル操作[編集]

構造体のシリアライズ[編集]

バイナリファイルへの書き込みと読み込み[編集]

構造体をバイナリファイルに書き込み、読み込むことで、データの永続化と復元が可能です。

#include <stdio.h>

struct Employee {
    int empId;
    char name[50];
    double salary;
} __attribute__((aligned(64)));

int main() {
    // 構造体をファイルに書き込む例
    struct Employee emp1 = {1, "John Doe", 50000.0};
    FILE *fp = fopen("employee.dat", "wbe");
    if (fp == NULL) {
        perror("Open: employee.dat:wb");
        return 1;
    }
    fwrite(&emp1, sizeof(struct Employee), 1, fp);
    fclose(fp);

    // ファイルから構造体を読み込む例
    struct Employee emp2;
    fp = fopen("employee.dat", "rbe");
    if (fp == NULL) {
        perror("Open: employee.dat:rb");
        return 1;
    }
    fread(&emp2, sizeof(struct Employee), 1, fp);
    fclose(fp);

    printf("Employee ID: %d\n", emp2.empId);
    printf("Name: %s\n", emp2.name);
    printf("Salary: %.2f\n", emp2.salary);

    return 0;
}

テキストファイルへの書き込みと読み込み[編集]

構造体をテキストファイルに書き込み、読み込むことも可能です。

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    double gpa;
} __attribute__((aligned(64)));

int main() {
    // 構造体をファイルに書き込む例
    struct Student students[3] = {
        {"Alice", 20, 3.5}, {"Bob", 21, 3.8}, {"Charlie", 19, 3.2}};

    FILE *fp = fopen("students.txt", "wbe");
    if (fp == NULL) {
        perror("Open: students.dat:wb");
        return 1;
    }
    for (int i = 0; i < 3; i++) {
        fprintf(fp, "%s %d %.2f\n", students[i].name, students[i].age,
                students[i].gpa);
    }
    fclose(fp);

    // ファイルから構造体を読み込む例
    struct Student student;
    fp = fopen("students.txt", "rbe");
    if (fp == NULL) {
        perror("Open: students.dat:rb");
        return 1;
    }
    while (fscanf(fp, "%s %d %lf", student.name, &student.age, &student.gpa) !=
           EOF) {
        printf("Name: %s, Age: %d, GPA: %.2f\n", student.name, student.age,
               student.gpa);
    }
    fclose(fp);

    return 0;
}

構造体の永続化[編集]

永続化の概念と必要性[編集]

データの永続化とは、プログラムの実行が終了してもデータを保存し、次回の実行時に復元できることを指します。構造体をファイルに書き込むことで、プログラムの実行状態を保存し、後から読み込むことができます。

永続化の実装例[編集]

前述のバイナリファイルやテキストファイルへの書き込みと読み込みの例は、永続化の実装例として非常に有用です。これにより、ユーザーが入力したデータやプログラムの状態を保存し、後で復元することが可能になります。

高度な構造体の使用[編集]

共用体との組み合わせ[編集]

共用体と構造体の違い[編集]

共用体は、同じメモリ領域を複数の異なる形式で解釈することができるデータ型です。これに対し、構造体は異なる種類のデータを一つのまとまりとして管理するためのものです。

共用体と構造体の組み合わせ例[編集]

共用体と構造体を組み合わせて、柔軟なデータ構造を設計することができます。

struct Data {
    int type;
    union {
        int intValue;
        double doubleValue;
        char stringValue[20];
    } value;
};

ビットフィールド[編集]

ビットフィールドの定義[編集]

ビットフィールドは、メモリ使用の最適化やデータの圧縮に役立ちます。

struct {
    unsigned int isStudent : 1;
    unsigned int age : 7;
} status;

メモリ効率を考えた構造体設計[編集]

ビットフィールドを使って、メモリ使用の最適化を図ることができます。ただし、ビットフィールドの使用はプラットフォーム依存性に注意が必要です。

練習問題と実践[編集]

基本的な練習問題[編集]

基本的な構造体操作に関する問題[編集]

  1. 点の座標を表す構造体Pointを定義し、2つの点の距離を計算する関数を作成してください。
  2. 学生情報を表す構造体Studentを定義し、3人分の学生の情報を入力して表示するプログラムを作成してください。

解答例と解説[編集]

  1. Point構造体を使って、2点間の距離を計算する関数を作成します。
    #include <stdio.h>
    #include <math.h>
    
    struct Point {
        double x;
        double y;
    };
    
    double distance(struct Point p1, struct Point p2) {
        return hypot(p2.x - p1.x, p2.y - p1.y);
    }
    
    int main() {
        struct Point p1 = {1, 2};
        struct Point p2 = {4, 6};
        printf("Distance between points: %.2f\n", distance(p1, p2));
        return 0;
    }
    
  2. Student構造体を使って、3人の学生の情報を入力して表示するプログラムを作成します。
    #include <stdio.h>
    
    struct Student {
        char name[50];
        int age;
        double gpa;
    };
    
    int main() {
        struct Student students[3];
    
        // 学生の情報を入力
        for (int i = 0; i < 3; i++) {
            printf("Enter student %d name: ", i + 1);
            scanf("%s", students[i].name);
            printf("Enter student %d age: ", i + 1);
            scanf("%d", &students[i].age);
            printf("Enter student %d GPA: ", i + 1);
            scanf("%f", &students[i].gpa);
        }
    
        // 入力した学生情報を表示
        printf("\nEntered student information:\n");
        for (int i = 0; i < 3; i++) {
            printf("Student %d\n", i + 1);
            printf("Name: %s\n", students[i].name);
            printf("Age: %d\n", students[i].age);
            printf("GPA: %.2f\n", students[i].gpa);
            printf("\n");
        }
    
        return 0;
    }
    

このプログラムでは、Student構造体を使って、3人分の学生の情報を入力し、それを表示する例です。

応用的な練習問題[編集]

実践的なプログラム設計に関する問題[編集]

  1. バンクアカウントを表す構造体BankAccountを定義し、預金と引き出しの操作を行う関数を実装してください。
  2. データベースのレコードを表す構造体DatabaseRecordを定義し、ファイルからデータを読み込み、データを更新するプログラムを作成してください。

解答例と解説[編集]

  1. BankAccount構造体を定義し、預金と引き出しの操作を行う関数を実装します。
    #include <stdio.h>
    
    struct BankAccount {
        int accountNumber;
        double balance;
    };
    
    void deposit(struct BankAccount *acc, double amount) {
        acc->balance += amount;
    }
    
    double withdraw(struct BankAccount *acc, double amount) {
        if (acc->balance >= amount) {
            acc->balance -= amount;
            return amount;
        } else {
            printf("Insufficient balance\n");
            return 0.0;
        }
    }
    
    int main() {
        struct BankAccount myAccount = {1001, 500.0};
    
        deposit(&myAccount, 200.0);
        printf("Current balance: %.2f\n", myAccount.balance);
    
        double withdrawnAmount = withdraw(&myAccount, 100.0);
        if (withdrawnAmount > 0.0) {
            printf("Withdrawn amount: %.2f\n", withdrawnAmount);
        }
        printf("Current balance: %.2f\n", myAccount.balance);
    
        return 0;
    }
    
  2. DatabaseRecord構造体を定義し、ファイルからデータを読み込み、データを更新するプログラムを作成します。以下は例としてファイルからの読み込みを示します。
    #include <stdio.h>
    #include <string.h>
    
    struct DatabaseRecord {
        int id;
        char name[50];
        int age;
    };
    
    int main() {
        FILE *fp = fopen("records.dat", "rb");
        if (fp == NULL) {
            perror("Error opening file");
            return 1;
        }
    
        struct DatabaseRecord record;
        while (fread(&record, sizeof(struct DatabaseRecord), 1, fp)) {
            printf("ID: %d\n", record.id);
            printf("Name: %s\n", record.name);
            printf("Age: %d\n", record.age);
            printf("\n");
        }
    
        fclose(fp);
        return 0;
    }
    

このプログラムは、records.datというバイナリファイルからDatabaseRecord構造体を読み込み、その内容を表示します。

プロジェクト[編集]

小規模なプロジェクト例[編集]

  1. シンプルなアドレス帳アプリケーションの作成
    構造体を使用して、名前、電話番号、住所などの情報を持つ連絡先を管理するシンプルなアドレス帳アプリケーションを作成してください。追加、編集、削除、検索などの基本的な操作ができるようにします。
  2. 簡易的な学生の成績管理システム
  3. 学生の名前、学籍番号、科目ごとの成績(数学、英語、科学など)を管理するプログラムを作成してください。成績の追加、削除、修正、平均成績の計算などの機能を実装します。

これらのプロジェクトは、構造体の基本的な操作から応用までを実践するための良い方法です。プロジェクトを通じて、実際の問題に対する解決策を設計し、プログラミングスキルを向上させることができます。

附録: 構造体リファレンス[編集]

構造体に関する標準ライブラリ関数[編集]

標準ライブラリ関数のリストと説明[編集]

C言語の標準ライブラリには、構造体操作に役立つ多くの関数が用意されています。例えば、memcpy関数やmemcmp関数は、構造体のメモリのコピーと比較に使用できます。

#include <string.h>

struct Person {
    char name[50];
    int age;
};

int main() {
    struct Person p1 = {"Alice", 25};
    struct Person p2;

    // 構造体のメモリをコピーする例 ※ p2 = p1 も可
    memcpy(&p2, &p1, sizeof(struct Person));

    // 構造体の比較を行う例
    if (memcmp(&p1, &p2, sizeof(struct Person)) == 0) {
        printf("p1 and p2 are equal\n");
    } else {
        printf("p1 and p2 are not equal\n");
    }

    return 0;
}

使用例[編集]

構造体を効率的に操作するための標準ライブラリ関数の使用例を示しています。これらの関数は、特に大規模なデータ処理や高度な操作が必要な場合に便利です。

よくあるエラーとその対処法[編集]

構造体に関する一般的なエラー[編集]

構造体の使用中によく見られるエラーとその対処法についてのヒントを提供します。例えば、ポインタを使った構造体の操作でのメモリリークやNULLポインタの処理などがあります。

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

struct Person {
    char name[50];
    int age;
};

void printPerson(struct Person *p) {
    if (p != NULL) {
        printf("Name: %s, Age: %d\n", p->name, p->age);
    } else {
        printf("Invalid person\n");
    }
}

int main() {
    struct Person *p = malloc(sizeof(struct Person));
    if (p == NULL) {
        perror("Failed to allocate memory");
        return 1;
    }

    strcpy(p->name, "Bob");
    p->age = 30;

    printPerson(p);

    free(p);
    return 0;
}

この附録は、標準ライブラリ関数のリファレンスと共に、構造体に関する一般的なエラーとその対処法を提供し、より効率的なプログラミングをサポートします。