配列
ロッカーの列を想像してください。それぞれ0から番号が振られ、同じ種類のものを収納しています——それが配列です。1つの名前で同種のデータをまとめて管理できます。
1次元配列の定義と初期化
配列は同じ型の要素がメモリ上に連続して並んだ順序付き集合です。配列を定義する際、要素の型と要素数を指定します。
int scores[5];
これは5つのint要素の配列を定義しますが、内容は未初期化で値は不定です。定義時に初期化することを推奨します。
int scores[5] = {90, 85, 78, 92, 88};
初期化リストが配列より短い場合、残りの要素は自動的に0になります。
int data[5] = {10, 20};
data[2]、data[3]、data[4]はすべて0です。この性質を利用して配列全体をゼロクリアできます。
int zeros[100] = {0};
完全な初期化リストを与える場合、配列の長さは省略でき、コンパイラが自動的に計算します。
int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
添字アクセス
配列要素は添字(インデックス)でアクセスします。添字は0から始まります。長さNの配列の有効な添字は0からN-1までです。
int scores[5] = {90, 85, 78, 92, 88};
int first = scores[0];
int last = scores[4];
scores[2] = 100;
添字には整数式が使えるため、ループでの走査に便利です。
int i = 3;
int val = scores[i];
境界外アクセスの危険性
Cは配列の添字が境界外かどうかをチェックしません。境界外要素へのアクセスは未定義動作です。ゴミデータを読んだり、他の変数を書き換えたり、プログラムをクラッシュさせたりする可能性があります。
int arr[3] = {10, 20, 30};
printf("%d\n", arr[3]);
printf("%d\n", arr[-1]);
どちらの行も不正な位置にアクセスしています。コンパイラは警告しませんが、結果は予測不可能です。
<=ではなく<を使いましょう。
配列の走査
forループで各要素を順番に処理するのが最も一般的な配列操作です。
int scores[5] = {90, 85, 78, 92, 88};
int i;
for (i = 0; i < 5; i++) {
printf("scores[%d] = %d\n", i, scores[i]);
}
全要素の合計:
int sum = 0;
for (i = 0; i < 5; i++) {
sum += scores[i];
}
printf("Total: %d\n", sum);
最大値を見つける:
int max = scores[0];
for (i = 1; i < 5; i++) {
if (scores[i] > max) {
max = scores[i];
}
}
printf("Highest: %d\n", max);
例
平均点と不合格者数を計算します。
#include <stdio.h>
int main(void) {
double scores[] = {78.5, 92.0, 55.5, 88.0, 43.0, 67.5, 90.0};
int len = sizeof(scores) / sizeof(scores[0]);
double sum = 0;
int fail = 0;
int i;
for (i = 0; i < len; i++) {
sum += scores[i];
if (scores[i] < 60.0) {
fail++;
}
}
printf("Average: %.1f\n", sum / len);
printf("Failing: %d\n", fail);
return 0;
}
Average: 73.5
Failing: 2
ここでsizeof(scores) / sizeof(scores[0])は要素数を動的に計算するため、配列の内容を変更してもループ条件を更新する必要がありません。
配列を関数の仮引数として渡す
配列を関数に渡す際、配列全体はコピーされず、最初の要素のアドレスだけが渡されます。つまり関数内でsizeofを使って配列の長さを知ることはできず、長さは別の仮引数として渡す必要があります。
void print_array(int arr[], int len) {
int i;
for (i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
仮引数int arr[]はint *arrと等価で、これがポインタへの「崩れ」を反映しています。関数内で配列要素を変更すると、元の配列にも影響します。同じメモリを操作しているためです。
void double_values(int arr[], int len) {
int i;
for (i = 0; i < len; i++) {
arr[i] *= 2;
}
}
int main(void) {
int data[] = {1, 2, 3, 4, 5};
double_values(data, 5);
return 0;
}
呼び出し後、dataは{2, 4, 6, 8, 10}になります。
2次元配列
2次元配列は「配列の配列」と考えられ、行列や表形式データの表現によく使われます。
int matrix[3][4];
これは3行4列の2次元配列を定義します。初期化は行ごとにグループ化できます。
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
フラットに書くこともできます。効果は同じです。
int matrix[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
2つの添字で要素にアクセスします。
int val = matrix[1][2];
matrix[0][0] = 100;
2次元配列の走査には二重ループが必要です。
int i, j;
for (i = 0; i < 3; i++) {
for (j = 0; j < 4; j++) {
printf("%4d", matrix[i][j]);
}
printf("\n");
}
初期化時、行数は省略できますが列数は省略できません。
int m[][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8}
};
コンパイラは初期化データから2行であることを推定します。
例
3人の学生の4科目の平均点を計算します。
#include <stdio.h>
void student_avg(double grades[][4], int rows) {
int i, j;
for (i = 0; i < rows; i++) {
double sum = 0;
for (j = 0; j < 4; j++) {
sum += grades[i][j];
}
printf("Student%d Average: %.1f\n", i + 1, sum / 4);
}
}
int main(void) {
double grades[3][4] = {
{85.0, 90.0, 78.0, 92.0},
{70.0, 65.0, 80.0, 75.0},
{95.0, 88.0, 92.0, 97.0}
};
student_avg(grades, 3);
return 0;
}
Student1 Average: 86.2
Student2 Average: 72.5
Student3 Average: 93.0
❓ よくある質問
sizeof(arr)を使って要素数を求められないのはなぜですか?sizeofはポインタのサイズ(4または8バイト)を返し、配列全体のサイズは返しません。📖 まとめ
- 配列は同じ型の要素の連続した格納領域で、添字は0から始まります
- 初期化時に長さは省略可能です。部分初期化では残りの要素が0で埋まります
- Cは境界チェックを行いません。プログラマが添字の有効性を保証する必要があります
- 配列は関数に渡すとポインタに崩れます。長さは別途渡す必要があります
- 2次元配列は行ごとに格納されます。関数の仮引数では列数の指定が必須です
📝 練習問題
- 10個の整数を配列に読み込み、すべての要素を逆順に出力するプログラムを書いてください。
- 整数配列とその長さを受け取り、最大値の添字を返す関数を書いてください。
- 4x4の整数行列を定義して初期化し、両対角線の和を計算してください。



