閉じる

C言語の2次元配列 入門: 宣言とアクセスの基本と例

C言語で複数の行と列を持つデータを扱うとき、2次元配列はもっとも素直で読みやすい選択です。

本記事では、2次元配列の宣言と初期化、そして要素アクセスを、初心者の方にも分かりやすい例とともに詳しく説明します。

ミスを防ぐための注意点や、実際に動かせるサンプルコードと出力例も用意しました。

C言語の2次元配列とは

表として考える

2次元配列は、行と列を持つ「表」として考えると理解しやすいです。

例えば int a[3][4] は、3行×4列の整数表です。

各要素は行インデックスと列インデックスで一意に指定できます。

次の表は、a[i][j] の概念を示しています。

値そのものよりも、行と列の位置を意識して見てください。

列0列1列2列3
行0a[0][0]a[0][1]a[0][2]a[0][3]
行1a[1][0]a[1][1]a[1][2]a[1][3]
行2a[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次元配列の宣言と初期化

宣言の基本形

宣言は型名 変数名[行数][列数]という形です。

もっとも基本的な例は次のとおりです。

C言語
// 3行4列のint型2次元配列を宣言
int a[3][4];

この時点では、aの中身は未初期化です。

使う前に必ず初期化または代入を行ってください。

サイズは定数で指定する

2次元配列のサイズは定数名で表現すると、意図が明確になりミスが減ります。

Cでは次の2つがよく使われます。

C言語
// 方法1: マクロ定義を使う
#define ROWS 3
#define COLS 4
int a[ROWS][COLS];

// 方法2: enumによる定数式
enum { ROWS2 = 3, COLS2 = 4 };
int b[ROWS2][COLS2];

#defineenumによる定数はコンパイル時定数として扱われ、静的配列のサイズに安全に使えます。

C99以降ではconst intを使った可変長配列(VLA)もありますが、初学者はまず定数サイズで始めることをおすすめします。

特に初期化子を使う配列やグローバル配列では定数が必須です。

波括弧{}での初期化

配列は波括弧で初期化できます。

2次元配列は入れ子の波括弧を用います。

C言語
#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行ずつ書くと読みやすくなります。

コメントで何行目かを示すのも実務でよく使われます。

C言語
// 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にしたい場合は、次の書き方がもっとも簡単で安全です。

C言語
int zero1[3][4] = { 0 }; // すべて0になる

グローバル変数やstaticな配列は、初期化を省略しても自動的に0になります。

ただし、ローカル変数の配列は初期化しないと未定義値です。

memsetでの強制ゼロ埋めは型によって注意が必要なので、初心者のうちは{0}もしくはループでの代入を使うと確実です。

要素へのアクセスとfor文

a[i][j]でアクセスする

アクセスはa[i][j]と書きます。

最初の添字が行、次の添字が列であることを再確認してください。

C言語
#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が基本です。

内側のループを列にすると、メモリアクセスが連続になって効率が良いです。

C言語
#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を使います。

C言語
#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[行][列]です。

行と列を入れ替えると、異なるメモリ位置にアクセスしてしまいます。

命名でrowcolと明示し、ループの外側を行、内側を列にすると混乱が減ります。

sizeofの落とし穴

配列のサイズ計算は便利ですが、関数に渡すと配列はポインタに「退化」するため、sizeofの結果が変わります。

まずは正しい求め方と、関数内での注意を実行例で確認しましょう。

C言語
#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次元配列を渡すときは行数と列数も一緒に渡すのが定石です。

C言語
#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");
    }
}

定数名を使って可読性を上げる

定数名は最強のドキュメントです。

ROWSCOLSの2語だけで、コードを読む人は配列の構造を即座に理解できます。

次のスタイルが実務で読みやすく、安全です。

C言語
#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次元配列の宣言とアクセスは確実で読みやすいコードになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!