C言語で表やマス目のようなデータを扱いたいとき、避けて通れないのが2次元配列です。
1次元配列は使ったことがあるけれど、2次元配列になると一気に難しく感じる方も多いと思います。
本記事では、2次元配列のイメージづくりから、宣言・初期化・アクセス方法・for文での走査まで、C言語初心者の方にもわかりやすいように、順を追って丁寧に解説していきます。
2次元配列とは?1次元配列との違いを理解しよう
2次元配列の基本イメージ
2次元配列は、「行」と「列」を持つ表形式のデータを扱うための配列です。
数学で出てくる行列や、エクセルの表、ゲームのマップのようなマス目をイメージすると理解しやすくなります。
例えば、3行4列の2次元配列は、頭の中では次のような表としてイメージするとよいです。
行番号を0~2、列番号を0~3とすると、概念的には次のように並びます。
| 列0 | 列1 | 列2 | 列3 | |
|---|---|---|---|---|
| 行0 | a[0][0] | a[0][1] | a[0][2] | a[0][3] |
| 行1 | a[1][0] | a[1][1] | a[1][2] | a[1][3] |
| 行2 | a[2][0] | a[2][1] | a[2][2] | a[2][3] |
このように、2次元配列は「行番号」と「列番号」の2つの添字で1つの要素を指定するところがポイントです。
1次元配列との違いと使い分け
1次元配列は、要素が1列に並んだデータ構造です。
要素はa[0], a[1], a[2] ...のように、1つの添字で表します。
2次元配列との主な違いは、次のように整理できます。
| 種類 | 添字の数 | 主なイメージ | 例 |
|---|---|---|---|
| 1次元配列 | 1つ | 一列に並んだ値の集まり | テストの点数一覧、1本のセンサー値 |
| 2次元配列 | 2つ(行・列) | 表、マス目、行列 | 座席表、画像のピクセル、ゲームのマップ |
2次元配列を使うべきなのは、「行と列の両方を意識して扱いたいデータ」の場合です。
例えば、席が20行×10列ある教室の座席番号を管理したり、5行3列の表計算を行ったりする場面では、2次元配列を使う方が自然でわかりやすくなります。
逆に、単に10人分の点数を並べるだけなら1次元配列で十分です。
データを頭の中で「表」として見たいかどうかが1つの判断基準になります。
2次元配列がよく使われる場面
2次元配列は、多くの場面で利用されます。
代表的な例をいくつか挙げます。
- 教室やホールの座席表
- 店舗や倉庫の棚配置表
- ゲームのマップ(マス目)や、オセロ・将棋などのボード
- 写真や画像のピクセルデータ(縦×横の明るさや色)
- 数学で扱う行列の計算
- 時間と項目の2軸で管理する表形式のログ
このように、縦方向と横方向の2つの番号で要素を指定したいとき、2次元配列は非常に便利です。
C言語の2次元配列の宣言方法
基本の書き方(int a[3][4]など)と各数字の意味
C言語における2次元配列の基本的な宣言は、次のようになります。
int a[3][4]; /* int型の2次元配列aを宣言 */
この宣言の意味を整理すると、次のようになります。
int: 各要素の型(ここではint型)a: 配列の名前[3]: 行の数(行数)[4]: 列の数(列数)
つまり、「3行4列のint型2次元配列a」を宣言していることになります。
要素の総数は3 × 4 = 12個です。
この12個の要素には、それぞれa[0][0]からa[2][3]までの添字でアクセスします。
行数と列数の考え方
C言語では、行番号と列番号の添字はどちらも0から始まることに注意が必要です。
行数をROWS、列数をCOLSとすると、次のように意識すると理解しやすくなります。
- 行番号の範囲 :
0 ~ ROWS - 1 - 列番号の範囲 :
0 ~ COLS - 1
この考え方を定数マクロを使って表現すると、次のようになります。
#include <stdio.h>
#define ROWS 3 /* 行数 */
#define COLS 4 /* 列数 */
int main(void)
{
int a[ROWS][COLS]; /* 3行4列の配列 */
/* 行番号は0~ROWS-1、列番号は0~COLS-1 */
printf("行番号の範囲: 0 ~ %d\n", ROWS - 1);
printf("列番号の範囲: 0 ~ %d\n", COLS - 1);
return 0;
}
行番号の範囲: 0 ~ 2
列番号の範囲: 0 ~ 3
「人間の数え方(1行目, 2行目)」と「C言語の添字(0から始まる)」はずれるので、特に初心者のうちは、頭の中でしっかりと切り替えることが大切です。
初期化の書き方
2次元配列は、宣言と同時に中身を指定して初期化することができます。
基本的な書き方は次の2通りがあります。
1. すべての要素を列挙する書き方
int a[2][3] = {
1, 2, 3,
4, 5, 6
};
この場合、値は1行目の左から右へ、次に2行目へという順番で自動的に割り当てられます。
つまり次のようになります。
a[0][0] = 1a[0][1] = 2a[0][2] = 3a[1][0] = 4a[1][1] = 5a[1][2] = 6
2. 行ごとに波かっこでまとめる書き方
こちらの方が表の形が目で見てわかりやすく、おすすめです。
int a[2][3] = {
{ 1, 2, 3 }, /* 1行目 */
{ 4, 5, 6 } /* 2行目 */
};
このように行ごとに波かっこで区切ると、どの行にどの値が入っているのかが一目でわかります。
部分的な初期化とゼロクリア
2次元配列では、一部の要素だけを指定して初期化することもできます。
指定していない要素は、自動的に0で埋められます(静的記憶域や初期化付きの自動変数の場合)。
1. 一部だけ指定する例
#include <stdio.h>
int main(void)
{
/* 部分的に初期化: 指定していない要素は0になる */
int a[2][3] = {
{ 1 }, /* 1行目は先頭だけ1、残りは0 */
{ 4, 5 } /* 2行目は2つだけ指定、残りは0 */
};
int i, j;
for (i = 0; i < 2; i++) {
for (j = 0; j < 3; j++) {
printf("a[%d][%d] = %d\n", i, j, a[i][j]);
}
}
return 0;
}
a[0][0] = 1
a[0][1] = 0
a[0][2] = 0
a[1][0] = 4
a[1][1] = 5
a[1][2] = 0
2. 全要素をゼロクリアする便利な書き方
全要素を0で初期化したい場合は、次のように{0}だけを書きます。
int a[3][4] = { 0 }; /* 3行4列のすべての要素が0になる */
これは、2次元配列を手軽に「ゼロクリア」するための定番テクニックです。
特に、カウンタやフラグを格納する配列を使うときによく使用されます。
2次元配列へのアクセス方法
インデックスの順番
2次元配列の要素にアクセスするときは、必ず「行番号」「列番号」の順で添字を書きます。
書式は常に次のようになります。
配列名[行番号][列番号]
例えば、3行4列の配列int a[3][4];にアクセスする場合は次のように書きます。
a[0][0]: 1行目1列目の要素a[1][2]: 2行目3列目の要素a[2][3]: 3行目4列目の要素
「行が先、列が後」という順番をしっかり覚えておくことが重要です。
具体例で見る要素アクセス
次のプログラムでは、2次元配列を初期化し、特定の要素にアクセスする例を示します。
#include <stdio.h>
int main(void)
{
int a[2][3] = {
{ 10, 20, 30 }, /* 1行目 */
{ 40, 50, 60 } /* 2行目 */
};
/* 特定の要素へアクセスして表示する */
printf("a[0][0] = %d\n", a[0][0]); /* 1行目1列目 */
printf("a[0][2] = %d\n", a[0][2]); /* 1行目3列目 */
printf("a[1][1] = %d\n", a[1][1]); /* 2行目2列目 */
/* 要素を書き換えることもできる */
a[1][2] = 999; /* 2行目3列目の値を999に変更 */
printf("a[1][2] を変更後: %d\n", a[1][2]);
return 0;
}
a[0][0] = 10
a[0][2] = 30
a[1][1] = 50
a[1][2] を変更後: 999
このように、2つの添字を指定することで、好きな位置の要素を自由に読み書きできることがわかります。
添字の範囲外アクセスで起こる危険
2次元配列の添字は、宣言した範囲内で使わなければなりません。
例えばint a[3][4];の場合、a[3][0]やa[0][4]のようなアクセスは範囲外アクセスになります。
範囲外アクセスは非常に危険で、プログラムが異常終了したり、予期しない動作をしたり、他の変数を書きつぶしたりする原因になります。
C言語では、範囲外アクセスを自動的にチェックしてくれないため、プログラマ自身が添字を正しく制御する必要があります。
避けるためのポイントとして、次のようなルールを徹底することが大切です。
- 添字
iを使うときは、必ず0 <= i && i < 行数を守る - 添字
jを使うときは、必ず0 <= j && j < 列数を守る - for文の条件式は
i < 行数、j < 列数という形式に統一する
添字の上限を<で比較する習慣をつけると、範囲外アクセスを防ぎやすくなります。
配列名と&a[0][0]の関係
2次元配列を扱うときには、メモリ上ではすべての要素が1列に並んで格納されているという視点も重要です。
例えばint a[2][3];は、メモリ上では次のように並びます。
| 位置 | 要素 |
|---|---|
| 先頭 | a[0][0] |
| a[0][1] | |
| a[0][2] | |
| a[1][0] | |
| a[1][1] | |
| 最後 | a[1][2] |
つまり、実際には「横方向(列方向)に連続」して、その後に次の行が続くという形で配置されています。
この配置順を行優先(row-major)と呼び、C言語の2次元配列はこの方式になっています。
ここで、2次元配列の配列名aと&a[0][0]の関係を整理します。
a: 型的には「3要素のint[3]の配列」へのポインタのように振る舞います&a[0][0]: 最初の要素(1行目1列目)のアドレス
メモリ上の先頭位置という意味では、aと&a[0][0]は同じアドレスを指す場合がほとんどですが、型は異なります。
初心者の段階では、「a[0][0]が2次元配列の一番左上の要素」「&a[0][0]はその場所のアドレス」という理解を持っていれば十分です。
ポインタとの詳しい関係は、ポインタ学習の段階で改めて深掘りするとよいです。
for文で回す2次元配列の走査パターン
二重for文の基本形
2次元配列をすべて走査(全要素を順に処理すること)するときは、二重for文を使うのが基本です。
典型的な書き方は次のようになります。
#include <stdio.h>
#define ROWS 2
#define COLS 3
int main(void)
{
int a[ROWS][COLS] = {
{ 1, 2, 3 },
{ 4, 5, 6 }
};
int i, j;
for (i = 0; i < ROWS; i++) { /* 行を0~ROWS-1まで回す */
for (j = 0; j < COLS; j++) { /* 列を0~COLS-1まで回す */
printf("a[%d][%d] = %d\n", i, j, a[i][j]);
}
}
return 0;
}
a[0][0] = 1
a[0][1] = 2
a[0][2] = 3
a[1][0] = 4
a[1][1] = 5
a[1][2] = 6
この形が2次元配列を扱うときの「基本パターン」です。
ほとんどの処理は、この二重for文を少し変えるだけで実現できます。
行優先で回す書き方と処理の流れ
上のプログラムでは、外側のfor文が行、内側のfor文が列を回していました。
この回し方は、行優先(row-major)での走査と呼ばれます。
処理の流れを文章で追うと、次のようになります。
i = 0のとき、j = 0, 1, 2と変化しながらa[0][0], a[0][1], a[0][2]を順番に処理する- 次に
i = 1となり、再びj = 0, 1, 2と変化しながらa[1][0], a[1][1], a[1][2]を順番に処理する
つまり、1行目を左から右へ処理し終わったら、2行目の左から右へ…という順番になります。
これは、C言語のメモリ配置と同じ順番なので、処理効率の面でも有利です。
列優先で回す書き方と違い
場合によっては、列を優先して処理したいこともあります。
そのときは、外側のfor文を列、内側のfor文を行として次のように書きます。
#include <stdio.h>
#define ROWS 2
#define COLS 3
int main(void)
{
int a[ROWS][COLS] = {
{ 1, 2, 3 },
{ 4, 5, 6 }
};
int i, j;
/* 列優先で走査する例 */
for (j = 0; j < COLS; j++) { /* 先に列を回す */
for (i = 0; i < ROWS; i++) { /* 次に行を回す */
printf("a[%d][%d] = %d\n", i, j, a[i][j]);
}
}
return 0;
}
a[0][0] = 1
a[1][0] = 4
a[0][1] = 2
a[1][1] = 5
a[0][2] = 3
a[1][2] = 6
この順番では、1列目を上から下へ処理し終わったら、2列目の上から下へ…という流れになります。
行列計算や、列ごとに合計を出したい場合など、列を意識した処理を行う際に便利です。
ただし、C言語の内部表現では行優先になっているため、列優先で走査するとメモリアクセスの効率が落ちることがあります。
初心者のうちは、まずは行優先で書くのが基本と考えてよいです。
2次元配列の表示(printf)のよくある書き方パターン
2次元配列を表として整った形で表示したい場合は、次のような二重for文の書き方がよく使われます。
#include <stdio.h>
#define ROWS 3
#define COLS 4
int main(void)
{
int a[ROWS][COLS] = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
int i, j;
printf("2次元配列の中身を表形式で表示します。\n");
for (i = 0; i < ROWS; i++) {
for (j = 0; j < COLS; j++) {
/* 幅を揃えて表示するために%3dと書いている */
printf("%3d ", a[i][j]);
}
printf("\n"); /* 1行分を表示し終えたら改行する */
}
return 0;
}
2次元配列の中身を表形式で表示します。
1 2 3 4
5 6 7 8
9 10 11 12
ポイントは、内側のfor文で1行分の要素を表示し、外側のfor文の1回転ごとに改行を行うことです。
このパターンは、2次元配列の内容をデバッグしたいときにも非常に役立ちます。
入力(scanf)や計算処理に2次元配列を使う例
2次元配列は、単に表示するだけでなく、ユーザー入力を受け取ったり、行ごと・列ごとの合計を計算したりするときにもよく使われます。
ここでは、テストの点数(クラス2組、各3人、教科2つ)を2次元配列で管理し、入力と計算を行う例を示します。
#include <stdio.h>
#define CLASSES 2 /* クラス数(行) */
#define STUDENTS 3 /* 各クラスの人数(列) */
int main(void)
{
int score[CLASSES][STUDENTS]; /* 各クラス各生徒の点数 */
int i, j;
/* 点数の入力 */
printf("各クラスの各生徒の点数を入力してください。\n");
for (i = 0; i < CLASSES; i++) {
printf("%d組目の点数を入力します。\n", i + 1); /* 人間向けには1から表示 */
for (j = 0; j < STUDENTS; j++) {
printf(" 生徒%dの点数: ", j + 1);
scanf("%d", &score[i][j]); /* score[i][j] へ入力 */
}
}
/* 各クラスごとの合計点と平均点を計算 */
for (i = 0; i < CLASSES; i++) {
int sum = 0;
for (j = 0; j < STUDENTS; j++) {
sum += score[i][j]; /* 各生徒の点数を合計する */
}
printf("%d組の合計点: %d, 平均点: %.2f\n",
i + 1, sum, (double)sum / STUDENTS);
}
return 0;
}
各クラスの各生徒の点数を入力してください。
1組目の点数を入力します。
生徒1の点数: 70
生徒2の点数: 80
生徒3の点数: 90
2組目の点数を入力します。
生徒1の点数: 60
生徒2の点数: 75
生徒3の点数: 85
1組の合計点: 240, 平均点: 80.00
2組の合計点: 220, 平均点: 73.33
この例では、「クラス番号」と「生徒番号」の2つの軸を2次元配列の[行][列]で表現しています。
このように、2次元配列を使うことで、「何番目のクラスの何番目の生徒の点数か」を自然に表現できるようになります。
まとめ
2次元配列は、表やマス目データを扱うための基本的な仕組みであり、C言語プログラミングでは頻繁に登場します。
1次元配列との違いは、要素を2つの添字(行・列)で指定する点です。
宣言の形型 配列名[行数][列数]、初期化の書き方、行優先の二重for文をしっかり身につければ、座席表やテーブル計算、ゲームの盤面など、さまざまな場面に応用できます。
まずは小さな2次元配列から、宣言・アクセス・表示・入力といった一連の動作を手を動かしながら確認し、自然に扱えるようになることを目指してください。
