C言語で複数の行と列を持つデータを扱うとき、2次元配列はもっとも素直で読みやすい選択です。
本記事では、2次元配列の宣言と初期化、そして要素アクセスを、初心者の方にも分かりやすい例とともに詳しく説明します。
ミスを防ぐための注意点や、実際に動かせるサンプルコードと出力例も用意しました。
C言語の2次元配列とは
表として考える
2次元配列は、行と列を持つ「表」として考えると理解しやすいです。
例えば int a[3][4]
は、3行×4列の整数表です。
各要素は行インデックスと列インデックスで一意に指定できます。
次の表は、a[i][j]
の概念を示しています。
値そのものよりも、行と列の位置を意識して見てください。
列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] |
最初の添字が行、次の添字が列という順序を常に意識すると、後述のfor文による処理も自然に書けます。
添字は0から始まる
C言語の配列は添字が0から始まる点が重要です。
int a[3][4]
でアクセスできる行は0~2、列は0~3です。
例えば、a[3][0]
やa[0][4]
は範囲外であり、未定義動作になります。
境界条件では<
と>
を使い、<=
や>=
にしないことが大切です。
配列の配列というイメージ
2次元配列は「配列の配列」です。
int a[3][4]
は「要素数3の配列」であり、その各要素は「要素数4のint
配列」です。
行ごとにまとまってメモリ上に配置されます(行優先、row-major)。
そのため、内側の添字(列)を内側のループで回すと、メモリアクセスが連続的になり、一般に効率が良いです。
2次元配列の宣言と初期化
宣言の基本形
宣言は型名 変数名[行数][列数]
という形です。
もっとも基本的な例は次のとおりです。
// 3行4列のint型2次元配列を宣言
int a[3][4];
この時点では、a
の中身は未初期化です。
使う前に必ず初期化または代入を行ってください。
サイズは定数で指定する
2次元配列のサイズは定数名で表現すると、意図が明確になりミスが減ります。
Cでは次の2つがよく使われます。
// 方法1: マクロ定義を使う
#define ROWS 3
#define COLS 4
int a[ROWS][COLS];
// 方法2: enumによる定数式
enum { ROWS2 = 3, COLS2 = 4 };
int b[ROWS2][COLS2];
#defineとenumによる定数はコンパイル時定数として扱われ、静的配列のサイズに安全に使えます。
C99以降ではconst int
を使った可変長配列(VLA)もありますが、初学者はまず定数サイズで始めることをおすすめします。
特に初期化子を使う配列やグローバル配列では定数が必須です。
波括弧{}での初期化
配列は波括弧で初期化できます。
2次元配列は入れ子の波括弧を用います。
#include <stdio.h>
int main(void) {
// 完全に埋める初期化
int full[2][3] = {
{ 1, 2, 3 },
{ 4, 5, 6 }
};
// 省略初期化: 指定していない要素は0になる
int partial[3][4] = {
{ 1, 2 }, // 残りの列は0
{ 3 }, // 残りの列は0
// 3行目全体は0
};
// 動作確認
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
printf("%d ", partial[i][j]);
}
printf("\n");
}
return 0;
}
1 2 0 0
3 0 0 0
0 0 0 0
省略した部分が0で埋まることが確認できます。
行ごとの初期化の書き方
行単位で見やすく初期化するには、次のように行を1行ずつ書くと読みやすくなります。
コメントで何行目かを示すのも実務でよく使われます。
// 3行4列: 行ごとの初期化
int tbl[3][4] = {
/* row 0 */ { 10, 11, 12, 13 },
/* row 1 */ { 20, 21, 22, 23 },
/* row 2 */ { 30, 31, 32, 33 }
};
また、特定の位置だけを明示して初期化する指定子
(designated initializer)もありますが、初心者のうちは上記の素直な形が分かりやすいです。
すべて0で初期化する方法
2次元配列を全要素0にしたい場合は、次の書き方がもっとも簡単で安全です。
int zero1[3][4] = { 0 }; // すべて0になる
グローバル変数やstatic
な配列は、初期化を省略しても自動的に0になります。
ただし、ローカル変数の配列は初期化しないと未定義値です。
memsetでの強制ゼロ埋めは型によって注意が必要なので、初心者のうちは{0}
もしくはループでの代入を使うと確実です。
要素へのアクセスとfor文
a[i][j]でアクセスする
アクセスはa[i][j]
と書きます。
最初の添字が行、次の添字が列であることを再確認してください。
#include <stdio.h>
int main(void) {
int a[3][4] = {
{ 1, 2, 3, 4 },
{ 10, 20, 30, 40 },
{ 7, 8, 9, 10 }
};
// 例: 行1, 列2の要素を表示
printf("a[1][2] = %d\n", a[1][2]); // 30
return 0;
}
a[1][2] = 30
二重forで全要素を処理する
2次元配列の全要素を走査するには、二重forが基本です。
内側のループを列にすると、メモリアクセスが連続になって効率が良いです。
#include <stdio.h>
#define ROWS 3
#define COLS 4
int main(void) {
int a[ROWS][COLS];
// 値を代入: 例として行*10 + 列
for (int i = 0; i < ROWS; ++i) {
for (int j = 0; j < COLS; ++j) {
a[i][j] = i * 10 + j;
}
}
// 表形式に出力
for (int i = 0; i < ROWS; ++i) {
for (int j = 0; j < COLS; ++j) {
printf("%2d ", a[i][j]);
}
printf("\n");
}
return 0;
}
0 1 2 3
10 11 12 13
20 21 22 23
入出力の例
標準入力から値を読み取り、行ごとの合計と一緒に出力する例です。
サイズは定数で決め、読み取りはscanf
を使います。
#include <stdio.h>
#define R 2
#define C 3
int main(void) {
int m[R][C];
// 入力: 2行×3列の整数を読み込む
// 例の入力はコメント参照
// 1 2 3
// 4 5 6
for (int i = 0; i < R; ++i) {
for (int j = 0; j < C; ++j) {
if (scanf("%d", &m[i][j]) != 1) {
// 入力エラーの簡易処理
fprintf(stderr, "入力エラーです。\n");
return 1;
}
}
}
// 出力: 行ごとの合計も表示
for (int i = 0; i < R; ++i) {
int row_sum = 0;
for (int j = 0; j < C; ++j) {
printf("%d ", m[i][j]);
row_sum += m[i][j];
}
printf("| row% d sum = %d\n", i, row_sum);
}
return 0;
}
出力例(入力が「1 2 3 4 5 6」の場合):
1 2 3 | row 0 sum = 6
4 5 6 | row 1 sum = 15
よくあるミスと注意点
添字の範囲
範囲外アクセスは未定義動作で、クラッシュや意図しない動作の原因になります。
ループ条件はi < 行数
、j < 列数
のように<
を使い、<=
を使わないことを徹底してください。
行と列の順序を間違えない
宣言がint a[ROWS][COLS]
なら、アクセスはa[行][列]
です。
行と列を入れ替えると、異なるメモリ位置にアクセスしてしまいます。
命名でrow
、col
と明示し、ループの外側を行、内側を列にすると混乱が減ります。
sizeofの落とし穴
配列のサイズ計算は便利ですが、関数に渡すと配列はポインタに「退化」するため、sizeof
の結果が変わります。
まずは正しい求め方と、関数内での注意を実行例で確認しましょう。
#include <stdio.h>
#define ROWS 3
#define COLS 4
// 配列を受け取るパラメータは「配列へのポインタ」に調整される
void show_in_func(int a[][COLS]) {
// ここでのsizeof(a)は「ポインタのサイズ」(多くの環境で8バイト)になる
printf("関数内 sizeof(a) = %zu (ポインタのサイズ)\n", sizeof(a));
printf("関数内 sizeof(a[0]) = %zu (COLS個のintのサイズ)\n", sizeof(a[0]));
printf("関数内 sizeof(a[0][0]) = %zu (intのサイズ)\n", sizeof(a[0][0]));
}
int main(void) {
int a[ROWS][COLS] = {0};
// 配列が見えているこのスコープでは、総サイズから行数・列数を求められる
printf("main内 sizeof(a) = %zu\n", sizeof(a));
printf("main内 sizeof(a[0]) = %zu\n", sizeof(a[0]));
printf("main内 sizeof(a[0][0]) = %zu\n", sizeof(a[0][0]));
size_t rows = sizeof(a) / sizeof(a[0]); // 行数
size_t cols = sizeof(a[0]) / sizeof(a[0][0]); // 列数
printf("推定 行数=%zu, 列数=%zu\n", rows, cols);
show_in_func(a);
return 0;
}
実行結果例(環境により異なるが一例):
main内 sizeof(a) = 48
main内 sizeof(a[0]) = 16
main内 sizeof(a[0][0]) = 4
推定 行数=3, 列数=4
関数内 sizeof(a) = 8 (ポインタのサイズ)
関数内 sizeof(a[0]) = 16 (COLS個のintのサイズ)
関数内 sizeof(a[0][0]) = 4 (intのサイズ)
関数の中では「総行数」をsizeofで求められません。
したがって、関数に2次元配列を渡すときは行数と列数も一緒に渡すのが定石です。
#include <stdio.h>
#define COLS 4
// 行数は別引数で渡す。列数は宣言で固定(例: COLS)
void print_matrix(int rows, int a[][COLS]) {
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < COLS; ++j) {
printf("%d ", a[i][j]);
}
printf("\n");
}
}
定数名を使って可読性を上げる
定数名は最強のドキュメントです。
ROWS
とCOLS
の2語だけで、コードを読む人は配列の構造を即座に理解できます。
次のスタイルが実務で読みやすく、安全です。
#include <stdio.h>
// 1. 配列サイズは定数で表す
enum { ROWS = 3, COLS = 4 };
// 2. 命名はrow/colを明示
void fill_seq(int a[ROWS][COLS]) {
for (int row = 0; row < ROWS; ++row) {
for (int col = 0; col < COLS; ++col) {
a[row][col] = row * 10 + col;
}
}
}
void print_table(const int a[ROWS][COLS]) {
for (int row = 0; row < ROWS; ++row) {
for (int col = 0; col < COLS; ++col) {
printf("%2d ", a[row][col]);
}
printf("\n");
}
}
int main(void) {
int a[ROWS][COLS] = {0};
fill_seq(a);
print_table(a);
return 0;
}
0 1 2 3
10 11 12 13
20 21 22 23
このように定数化と命名の一貫性を保つと、バグの混入を大幅に防げます。
まとめ
2次元配列は、C言語で表形式のデータを扱うための基本的な道具です。
行と列の順序を守り、サイズは定数で管理し、二重forで処理するという3点を押さえるだけで、ほとんどの用途に対応できます。
初期化は波括弧を使い、ゼロ初期化は{0}
を選べば安全です。
また、sizeofはスコープによって意味が変わることを理解し、関数には行数・列数も渡す習慣をつけてください。
これらを身につければ、2次元配列の宣言とアクセスは確実で読みやすいコードになります。