Step 4: 構造体配列と構造体ポインタ
複数のデータをまとめて扱おう!
こんにちは! C言語学習、順調に進んでいますか? 前回は、関連する複数のデータをひとまとめにする「構造体(struct)」について学びましたね。構造体を使うことで、例えば「学生」の情報(名前、学籍番号、点数など)を一つの型として定義できるようになりました。
今回は、その構造体をさらに便利に使うためのテクニック、「構造体配列」と「構造体ポインタ」について学んでいきましょう。これらをマスターすれば、たくさんのデータを効率的に扱えるようになり、プログラムの表現力が格段にアップしますよ!
1. 構造体配列:たくさんの構造体をまとめて管理!
「構造体配列」とは、その名の通り、同じ型の構造体を複数個まとめて格納できる配列のことです。例えば、クラス全員の学生情報や、お店の商品リストなどを管理したい場合に非常に役立ちます。
構造体配列の宣言
構造体配列の宣言は、基本的なデータ型(int型やchar型など)の配列宣言とよく似ています。構造体タグ名に続けて、配列名と要素数を指定します。
// まずは構造体を定義 (前回の復習)
struct Student { char name[50]; int id; int score;
};
// Student型の構造体配列を宣言 (要素数3)
struct Student class_a[3];
上記の例では、Student
という構造体(名前、ID、点数を持つ)を3人分格納できる配列 class_a
を宣言しています。これで、class_a[0]
, class_a[1]
, class_a[2]
という3つの Student
型の構造体変数が用意されたことになります。
構造体配列の初期化
構造体配列も、宣言時に初期化することができます。配列の初期化と同様に、{}
を使って各要素の初期値を指定します。各要素は構造体なので、さらに {}
でメンバの値を指定します。
#include <stdio.h>
#include <string.h> // strcpy を使うために必要
// 構造体の定義
struct Student { char name[50]; int id; int score;
};
int main() { // 構造体配列の宣言と初期化 struct Student class_a[3] = { {"Alice", 101, 85}, // class_a[0] の初期化 {"Bob", 102, 92}, // class_a[1] の初期化 {"Charlie", 103, 78} // class_a[2] の初期化 }; // 初期化された値を確認 (例: 最初の学生の情報) printf("最初の学生:\n"); printf(" 名前: %s\n", class_a[0].name); printf(" ID: %d\n", class_a[0].id); printf(" 点数: %d\n", class_a[0].score); return 0;
}
name
)の初期化は上記のように直接文字列リテラルで行えますが、宣言後に文字列を代入する場合は strcpy
関数などを使う必要があります。 // これはエラーになることが多い
// class_a[0].name = "New Name";
// strcpy を使って代入する
strcpy(class_a[0].name, "New Name");
(strcpy
については Step 5 で詳しく学びます) もちろん、宣言後に個別に値を代入することも可能です。
#include <stdio.h>
#include <string.h>
struct Student { char name[50]; int id; int score;
};
int main() { struct Student class_b[2]; // 要素数2で宣言 (初期化はしない) // class_b[0] に値を代入 strcpy(class_b[0].name, "David"); class_b[0].id = 104; class_b[0].score = 88; // class_b[1] に値を代入 strcpy(class_b[1].name, "Eve"); class_b[1].id = 105; class_b[1].score = 95; printf("class_b[0]: Name=%s, ID=%d, Score=%d\n", class_b[0].name, class_b[0].id, class_b[0].score); printf("class_b[1]: Name=%s, ID=%d, Score=%d\n", class_b[1].name, class_b[1].id, class_b[1].score); return 0;
}
構造体配列の要素へのアクセス
構造体配列の特定の要素にあるメンバにアクセスするには、まず配列のインデックスを使って要素を指定し、その後にドット演算子 .
を使ってメンバを指定します。
書式: 配列名[インデックス].メンバ名
#include <stdio.h>
#include <string.h>
struct Product { char item_name[100]; int price; int stock;
};
int main() { struct Product items[3] = { {"Apple", 150, 20}, {"Banana", 100, 30}, {"Orange", 120, 15} }; // 2番目の商品 (インデックス1) の価格を表示 printf("2番目の商品の価格: %d 円\n", items[1].price); // Banana の価格 // 3番目の商品 (インデックス2) の在庫数を変更 items[2].stock = 10; printf("3番目の商品の在庫数: %d 個\n", items[2].stock); // Orange の在庫数 // forループを使って全商品の情報を表示 printf("\n--- 商品リスト ---\n"); for (int i = 0; i < 3; i++) { printf("商品名: %s, 価格: %d, 在庫: %d\n", items[i].item_name, items[i].price, items[i].stock); } return 0;
}
このように、for
ループと組み合わせることで、構造体配列の全要素に対して同じ処理を簡単に記述できます。これが構造体配列の大きなメリットの一つです。
2. 構造体ポインタ:構造体のアドレスを指し示す
次に、「構造体ポインタ」について学びましょう。ポインタは C 言語の強力な機能ですが、少し難しく感じるかもしれませんね。安心してください、一つずつ丁寧に見ていきましょう!
「構造体ポインタ」とは、構造体変数が格納されているメモリアドレスを保持するためのポインタ変数です。通常のポインタ(int型ポインタなど)が int 型変数のアドレスを指すのと同じように、構造体ポインタは構造体変数のアドレスを指します。
構造体ポインタの宣言
構造体ポインタの宣言は、構造体タグ名の後にアスタリスク *
をつけてポインタ変数名を記述します。
struct Student { // 上で定義した Student 構造体を再利用 char name[50]; int id; int score;
};
int main() { struct Student alice = {"Alice", 101, 85}; // 構造体変数 struct Student bob = {"Bob", 102, 92}; // 別の構造体変数 // Student 型の構造体ポインタを宣言 struct Student *ptr_student; // ポインタに alice のアドレスを代入 ptr_student = &alice; // アドレス演算子 & を使用 // これで ptr_student は alice を指している状態になる // あとで bob のアドレスを代入することも可能 // ptr_student = &bob; return 0;
}
上記の例では、struct Student *ptr_student;
で Student
型の構造体を指すことができるポインタ変数 ptr_student
を宣言しています。そして、ptr_student = &alice;
で、構造体変数 alice
のメモリアドレスを ptr_student
に代入しています。
構造体ポインタを使ったメンバへのアクセス
構造体ポインタを使って、それが指し示している構造体のメンバにアクセスするには、主に2つの方法があります。
- 間接参照演算子
*
とドット演算子.
の組み合わせ
まず、ポインタ変数名の前に*
をつけてポインタが指す構造体そのもの(実体)を取り出し(間接参照)、その後にドット演算子.
を使ってメンバにアクセスします。このとき、*ptr_student
を括弧()
で囲む必要がある点に注意してください。これは演算子の優先順位のためです(.
の方が*
より優先度が高い)。
書式:(*ポインタ変数名).メンバ名
- アロー演算子
->
構造体ポインタ専用の演算子として、アロー演算子->
が用意されています。これは、上記の(*ポインタ変数名).メンバ名
と全く同じ意味ですが、より簡潔に記述できます。一般的に、構造体ポインタ経由でメンバにアクセスする場合は、アロー演算子が使われます。
書式:ポインタ変数名->メンバ名
#include <stdio.h>
#include <string.h>
struct Student { char name[50]; int id; int score;
};
int main() { struct Student alice = {"Alice", 101, 85}; struct Student *ptr_student; // 構造体ポインタ ptr_student = &alice; // alice のアドレスを代入 // 方法1: (*ポインタ変数名).メンバ名 printf("方法1:\n"); printf(" 名前: %s\n", (*ptr_student).name); // (*ptr_student) を忘れずに! printf(" ID: %d\n", (*ptr_student).id); printf(" 点数: %d\n", (*ptr_student).score); // 方法2: ポインタ変数名->メンバ名 (アロー演算子) - こちらが一般的! printf("\n方法2 (アロー演算子):\n"); printf(" 名前: %s\n", ptr_student->name); printf(" ID: %d\n", ptr_student->id); printf(" 点数: %d\n", ptr_student->score); // アロー演算子を使ってメンバの値を変更することも可能 ptr_student->score = 90; // Alice の点数を 90 に変更 printf("\n変更後の点数 (alice.score): %d\n", alice.score); // alice の値も変わっていることを確認 printf("変更後の点数 (ptr_student->score): %d\n", ptr_student->score); return 0;
}
ポイント: アロー演算子 ->
は、構造体ポインタからメンバにアクセスするための便利なショートカットです。(*p).m
と書く代わりに p->m
と書けるので、コードがすっきりし、読みやすくなります。積極的に使っていきましょう!
3. 構造体配列と構造体ポインタの組み合わせ
構造体配列と構造体ポインタを組み合わせることで、さらに柔軟なデータ操作が可能になります。特に、配列の要素を順番に処理していく際に便利です。
配列名はその配列の先頭要素のアドレスを表す、というルールを思い出してください。これは構造体配列でも同じです。つまり、構造体配列の名前は、その配列の最初の要素(配列名[0]
)を指す構造体ポインタとして扱うことができます。
#include <stdio.h>
struct Point { int x; int y;
};
int main() { struct Point path[3] = { {1, 2}, {3, 4}, {5, 6} }; struct Point *ptr; // 配列名 path は先頭要素 path[0] のアドレスを示す ptr = path; // ptr = &path[0]; と同じ意味 // ポインタ ptr を使って先頭要素のメンバにアクセス printf("最初の点 (ptr): x=%d, y=%d\n", ptr->x, ptr->y); printf("最初の点 (path[0]): x=%d, y=%d\n", path[0].x, path[0].y); // ポインタ演算を使って次の要素へ移動 ptr++; // ポインタを次の Point 要素のアドレスに進める // ptr は現在 path[1] を指している printf("次の点 (ptr): x=%d, y=%d\n", ptr->x, ptr->y); printf("次の点 (path[1]): x=%d, y=%d\n", path[1].x, path[1].y); // ループで構造体配列を走査する例 printf("\nループで全要素を表示:\n"); ptr = path; // ポインタを先頭に戻す for (int i = 0; i < 3; i++) { // アロー演算子でアクセス printf(" 点 %d: x=%d, y=%d\n", i, ptr->x, ptr->y); ptr++; // 次の要素へ } // ポインタ演算を使わずにインデックスでアクセスする方が、通常は分かりやすい printf("\nループで全要素を表示 (インデックス):\n"); for (int i = 0; i < 3; i++) { // 配列とインデックスでアクセス printf(" 点 %d: x=%d, y=%d\n", i, path[i].x, path[i].y); } // ポインタを使って特定の要素のアドレスを取得 ptr = &path[2]; // 3番目の要素のアドレスを取得 printf("\n3番目の点 (ptr): x=%d, y=%d\n", ptr->x, ptr->y); return 0;
}
ポインタ演算(ptr++
など)を使って構造体配列の要素を順番にアクセスすることもできます。ptr++
とすると、ポインタは sizeof(struct Point)
バイトだけ進み、次の要素のアドレスを指すようになります。これは、ポインタが指している型(この場合は struct Point
)のサイズを自動的に計算してくれるためです。
ただし、単純に配列の全要素を順番に処理する場合は、for
ループとインデックス(path[i]
)を使う方が、コードが直感的で理解しやすいことが多いです。ポインタ演算は、特定の状況(例: 関数に配列の一部だけを渡したい場合など)で役立ちますが、無理に使う必要はありません。
4. 関数との連携:構造体を効率的に渡す
構造体配列や構造体ポインタは、関数にデータを渡す際にも非常に重要です。
関数に構造体配列を渡す
関数に構造体配列を渡す場合、通常の配列と同様に、配列名を引数として渡します。関数側では、引数を構造体ポインタとして受け取るか、あるいはサイズを指定しない配列として受け取ることができます。配列名を渡すと、実際には配列の先頭要素のアドレスが渡される(参照渡しに近い動作)ため、関数内で配列の要素を変更すると、呼び出し元の配列も変更されます。
#include <stdio.h>
struct Student { char name[50]; int score;
};
// 関数: 構造体配列を受け取り、全員の点数を表示する
// 仮引数はポインタ (struct Student *arr) または 配列 (struct Student arr[]) で受け取れる
// 配列の要素数も一緒に渡す必要がある
void print_scores(struct Student arr[], int size) { printf("--- 点数リスト ---\n"); for (int i = 0; i < size; i++) { // 配列としてアクセス printf("名前: %s, 点数: %d\n", arr[i].name, arr[i].score); }
}
// 関数: 全員の点数を 10 点上げる
void add_bonus(struct Student *arr, int size) { printf("\n--- 10点加算中... ---\n"); for(int i = 0; i < size; i++){ // ポインタ演算を使う例 (arr[i] でも同じ) (arr + i)->score += 10; // または、配列としてアクセスする例 // arr[i].score += 10; }
}
int main() { struct Student class_c[3] = { {"Ken", 75}, {"Lisa", 88}, {"Mike", 62} }; int num_students = 3; // 配列を関数に渡す (配列名 = 先頭要素のアドレス) print_scores(class_c, num_students); // 点数を加算する関数を呼び出す add_bonus(class_c, num_students); // 変更が反映されているか確認 printf("\n加算後の点数:\n"); print_scores(class_c, num_students); return 0;
}
size
のように、配列の要素数を別の引数として渡すのが一般的です。 関数に構造体ポインタを渡す
個々の構造体変数を関数に渡す場合、構造体そのものをコピーして渡す「値渡し」と、構造体のアドレス(ポインタ)を渡す「参照渡し(ポインタ渡し)」の2つの方法があります。
- 値渡し: 構造体全体がコピーされて関数に渡されます。関数内でメンバを変更しても、呼び出し元の構造体には影響しません。構造体のサイズが大きい場合、コピーのコストがかかる可能性があります。
- 参照渡し(ポインタ渡し): 構造体のアドレス(ポインタ)だけが関数に渡されます。関数内でポインタ(アロー演算子
->
)を使ってメンバを変更すると、呼び出し元の構造体も変更されます。コピーのコストが小さいのがメリットです。
多くの場合、特に構造体のサイズが大きい場合や、関数内で元の構造体を変更したい場合には、ポインタを渡す方法(参照渡し)が効率的でよく使われます。
#include <stdio.h>
#include <string.h>
struct Student { char name[50]; int score;
};
// 値渡し: 構造体そのものを受け取る関数
void print_student_value(struct Student s) { printf("--- 値渡し ---\n"); printf("名前: %s, 点数: %d\n", s.name, s.score); // ここで s.score を変更しても、呼び出し元の student1 は変わらない s.score = 0;
}
// 参照渡し(ポインタ渡し): 構造体のポインタを受け取る関数
void print_student_pointer(struct Student *s_ptr) { printf("--- ポインタ渡し ---\n"); // アロー演算子でアクセス printf("名前: %s, 点数: %d\n", s_ptr->name, s_ptr->score);
}
// 参照渡し(ポインタ渡し): 点数を変更する関数
void update_score_pointer(struct Student *s_ptr, int new_score) { printf("--- 点数更新 (ポインタ渡し) ---\n"); s_ptr->score = new_score; // アロー演算子で元の構造体の値を変更 printf("関数内で更新後の点数: %d\n", s_ptr->score);
}
int main() { struct Student student1 = {"Yuki", 95}; // 値渡しで関数呼び出し print_student_value(student1); printf("値渡し後、mainでの点数: %d (変わらない)\n\n", student1.score); // ポインタ渡しで関数呼び出し print_student_pointer(&student1); // アドレスを渡す // ポインタ渡しで点数を更新する関数呼び出し update_score_pointer(&student1, 100); // アドレスを渡し、新しい点数を指定 printf("ポインタ渡し更新後、mainでの点数: %d (変わっている)\n", student1.score); return 0;
}
関数に構造体のポインタを渡すことで、関数内で元のデータを直接変更できることがわかりますね。これはプログラムを作る上で非常に重要なテクニックです。
まとめ
今回は、構造体をさらに活用するための「構造体配列」と「構造体ポインタ」について学びました。
- 構造体配列を使うと、同じ型の構造体を複数まとめて管理でき、ループ処理などで効率的に扱えます。宣言は
struct タグ名 配列名[要素数];
、アクセスは配列名[i].メンバ名
です。 - 構造体ポインタは、構造体変数のアドレスを格納するポインタです。宣言は
struct タグ名 *ポインタ名;
です。 - 構造体ポインタからメンバにアクセスするには、アロー演算子
->
(ポインタ名->メンバ名
) を使うのが一般的で便利です。(*ポインタ名).メンバ名
と同じ意味です。 - 関数に構造体を渡す際、大きな構造体や関数内で変更したい場合は、ポインタ渡し(アドレスを渡す)が効率的です。
これらの概念は、今後の学習、特にファイル操作やデータ構造、動的メモリ確保などで頻繁に登場します。少し複雑に感じるかもしれませんが、サンプルコードを実際に動かしながら、配列やポインタがどのように構造体と連携するのかをしっかり理解しておきましょう。
次回は、同じメモリ領域を複数の目的で共有する「共用体(union)」や、状態などを分かりやすく管理するための「列挙型(enum)」について学びます。お楽しみに!