C言語におけるプログラミングでは、異なる型のデータをひとまとめにする「構造体」と、同じ型のデータを連続して並べる「配列」は非常に頻繁に利用されます。
これら2つの概念を組み合わせた構造体配列は、住所録、商品の在庫管理、ゲーム内のキャラクター情報管理など、複雑なデータ群を効率的に扱うための強力な手段となります。
本記事では、構造体配列の基礎的な宣言方法から、初期化、要素へのアクセス、そしてポインタを用いた動的なメモリ確保まで、実践的なコード例を交えて詳しく解説します。
構造体配列の基本概念と宣言方法
C言語の構造体は、関連性のある複数の変数を1つの名前でグループ化するための仕組みです。
これに対し、配列は同一のデータ型を複数並べて管理する仕組みです。
これらを組み合わせることで、同じ属性を持つ複数のオブジェクトを一つの変数として管理できるようになります。
構造体の定義と基本的な配列の宣言
まずは、構造体そのものの定義と、それを配列として宣言する基本的な構文を確認しましょう。
例えば、学生の「学籍番号」と「点数」を管理する構造体 Student を考えます。
#include <stdio.h>
// 構造体の定義
struct Student {
int id; // 学籍番号
int score; // 点数
char name[20]; // 名前
};
int main() {
// 構造体配列の宣言(5人分のデータを保持可能)
struct Student classA[5];
return 0;
}
この例では、struct Student 型の要素を5個持つ配列 classA を作成しています。
メモリ上では、struct Student 1つ分のサイズが5個分連続して確保されることになります。
構造体配列の初期化
配列の宣言と同時に初期値を代入することも可能です。
ネストされた中括弧 {} を使用して、各要素の各メンバに値を割り当てます。
struct Student classB[3] = {
{1001, 85, "Tanaka"},
{1002, 92, "Sato"},
{1003, 78, "Suzuki"}
};
このように記述することで、各インデックスに対応するメンバ変数を一括で初期化できます。
要素数を省略して struct Student classB[] = { ... }; と記述した場合は、初期化子の数に合わせて自動的に配列サイズが決定されます。
構造体配列の要素へのアクセスと操作
宣言された構造体配列のデータを利用するには、配列のインデックス(添字)と、構造体メンバを指定するドット演算子 . を組み合わせます。
データの読み取りと書き込み
特定の要素にアクセスする場合、まず配列の何番目かを指定し、その後にどのメンバにアクセスするかを記述します。
#include <stdio.h>
#include <string.h>
struct Student {
int id;
int score;
char name[20];
};
int main() {
struct Student students[3];
// 0番目の要素に値を代入
students[0].id = 101;
students[0].score = 80;
strcpy(students[0].name, "Yamada");
// 1番目の要素に値を代入
students[1].id = 102;
students[1].score = 95;
strcpy(students[1].name, "Ito");
// データの表示
for (int i = 0; i < 2; i++) {
printf("ID: %d, Name: %s, Score: %d\n",
students[i].id, students[i].name, students[i].score);
}
return 0;
}
ID: 101, Name: Yamada, Score: 80
ID: 102, Name: Ito, Score: 95
注意点として、配列のインデックスは常に 0 から始まるため、要素数が 5 の配列であればアクセス可能な範囲は 0 から 4 までとなります。
範囲外へのアクセスは、メモリ破壊やプログラムの異常終了を引き起こす原因となるため、ループ処理などでは条件式を正確に記述する必要があります。
関数への構造体配列の受け渡し
大規模なプログラムでは、構造体配列の処理を関数に分割することが一般的です。
C言語において配列を関数に渡す場合、実際には配列の先頭アドレス(ポインタ)が渡されます。
ポインタを利用した引数設計
関数内で構造体配列を受け取るには、ポインタ型として引数を定義します。
また、配列の要素数を併せて渡すのが一般的な作法です。
#include <stdio.h>
struct Product {
int code;
int price;
};
// 構造体配列を表示する関数
void printProducts(struct Product *list, int size) {
for (int i = 0; i < size; i++) {
// 配列形式でアクセス可能
printf("商品コード: %d, 価格: %d円\n", list[i].code, list[i].price);
}
}
int main() {
struct Product items[2] = {
{5001, 1200},
{5002, 2500}
};
// 関数に配列を渡す
printProducts(items, 2);
return 0;
}
商品コード: 5001, 価格: 1200円
商品コード: 5002, 価格: 2500円
この方法の利点は、配列全体をコピーするのではなくアドレスのみを渡すため、メモリ使用量と処理負荷を最小限に抑えられる点にあります。
関数内で値を変更した場合は、呼び出し元の配列データも直接書き換わることになります。
ポインタによる構造体配列の動的確保
プログラムの実行時に、必要な要素数が決まる場合は、静的な配列ではなく動的メモリ確保を利用します。
これには malloc 関数を使用します。
mallocによるメモリ確保の基本
実行時にユーザー入力を受け取って、その数だけ構造体を作成するようなケースで非常に有効です。
#include <stdio.h>
#include <stdlib.h> // malloc, freeに必要
struct Item {
int id;
char category;
};
int main() {
int n;
printf("作成するアイテムの数を入力してください: ");
scanf("%d", &n);
// 構造体 n 個分のメモリを動的に確保
struct Item *itemArray = (struct Item *)malloc(sizeof(struct Item) * n);
if (itemArray == NULL) {
printf("メモリ確保に失敗しました。\n");
return 1;
}
// データの代入(アロー演算子ではなく配列記法が使いやすい)
for (int i = 0; i < n; i++) {
itemArray[i].id = i + 1;
itemArray[i].category = 'A';
}
// 表示
for (int i = 0; i < n; i++) {
printf("Item %d: ID=%d, Cat=%c\n", i, itemArray[i].id, itemArray[i].category);
}
// 確保したメモリの解放
free(itemArray);
return 0;
}
動的に確保したメモリは、使用が終わったら必ず free 関数で解放する必要があります。
これを怠ると、メモリリークが発生し、長時間の稼働によりシステムのメモリを使い果たしてしまう恐れがあります。
ポインタとアロー演算子の使い分け
ポインタを介して構造体のメンバにアクセスする場合、2通りの記述方法があります。
| 記述方法 | 構文例 | 意味 |
|---|---|---|
| アロー演算子 | ptr->id | ポインタ ptr が指す構造体のメンバ id |
| デリファレンスとドット | (\*ptr).id | ポインタ ptr の実体の中のメンバ id |
構造体配列をポインタで扱う際、特定の要素を指しているときはアロー演算子 -> を、配列全体の中でインデックス指定をするときは itemArray[i].id のようにドット演算子を使用するのが一般的です。
構造体配列の応用:ソートと検索
蓄積されたデータを活用するために、並び替え(ソート)や特定のデータの検索は不可欠な処理です。
バブルソートによるデータの並び替え
点数順に構造体配列を並び替える例を見てみましょう。
構造体ごと入れ替えるため、一時的な退避場所も同じ構造体型で用意します。
#include <stdio.h>
struct ScoreData {
int id;
int score;
};
void sortScores(struct ScoreData *data, int n) {
struct ScoreData temp;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (data[j].score < data[j + 1].score) {
// 構造体ごとスワップ(交換)
temp = data[j];
data[j] = data[j + 1];
data[j + 1] = temp;
}
}
}
}
int main() {
struct ScoreData scores[] = {
{1, 75}, {2, 90}, {3, 60}, {4, 85}
};
int n = 4;
sortScores(scores, n);
printf("降順ソート結果:\n");
for (int i = 0; i < n; i++) {
printf("ID:%d, Score:%d\n", scores[i].id, scores[i].score);
}
return 0;
}
降順ソート結果:
ID:2, Score:90
ID:4, Score:85
ID:1, Score:75
ID:3, Score:60
C言語では、構造体同士を代入文 = でコピーできるため、このように簡単に要素の入れ替えを行うことができます。
ただし、メンバに動的なメモリを指すポインタが含まれる場合は、「浅いコピー」による問題が生じるため注意が必要です。
構造体配列のメモリレイアウトとパディング
構造体配列を扱う上で、中級者以上の開発者が意識すべきなのが「メモリのアライメント」と「パディング」です。
構造体内の隙間
CPUは特定の境界(4バイトや8バイトなど)でデータを読み取るのが効率的であるため、構造体のサイズはメンバの単純な合計値にならないことがあります。
struct Sample {
char a; // 1バイト
int b; // 4バイト
};
この場合、sizeof(struct Sample) は 5 ではなく 8 になることが一般的です。
これは char a の後に3バイトの「パディング」という空きスペースが挿入されるためです。
配列における影響
構造体配列では、このパディングを含んだサイズが連続して並びます。
そのため、配列全体のサイズを求める際は必ず sizeof 演算子を使用するようにし、手計算でサイズを見積もるのは避けましょう。
int totalSize = sizeof(classA); // 配列全体のバイト数
int elementCount = sizeof(classA) / sizeof(classA[0]); // 要素数
このように記述することで、パディングが含まれていても正しく要素数を計算することができます。
まとめ
本記事では、C言語における構造体配列の宣言、初期化、ポインタを用いた動的なメモリ確保、そして実用的なソート処理まで網羅的に解説しました。
重要なポイントを振り返ると以下の通りです。
- 構造体配列は、同じデータ構造を持つ複数の情報を管理するのに最適である。
- 配列の要素へのアクセスは、
配列名[添字].メンバ名という形式で行う。 - 関数に渡す際はポインタを使用し、効率的なメモリアクセスを実現する。
- 要素数が可変の場合は、
mallocを使って動的にメモリを確保し、使用後は必ずfreeする。 - 構造体にはパディングが発生するため、サイズ計算には常に
sizeofを活用する。
構造体配列をマスターすることで、C言語によるプログラミングの幅は劇的に広がります。
単なる数値の羅列ではなく、意味のある「オブジェクトの集まり」としてデータを捉えられるようになるため、より高度なアプリケーション開発が可能になるでしょう。
ぜひ、実際のコードを書きながら、これらの挙動を体感してみてください。
