🇯🇵 日本語 | 🇺🇸 English
広告スペース

C言語 多次元配列の深掘り

行優先メモリレイアウトから関数渡し、動的確保、行列・画像処理まで。配列の基礎を終えた人向け。

💪 初心者は1回で分からなくて普通
多次元配列は「関数に渡す 4つのパターン」が混乱の元ですが、最初は Pattern A(連続領域)だけ覚えれば実用は足ります。
再挑戦のステップ
  1. 1次元配列ポインタ を復習
  2. 「行優先」のメモリ配置図を頭に焼き付ける
  3. 関数渡しは1つの方法だけ選んで使う (まずはフラット配列+cols)
  4. 実行速度の話は後回し (まず正しく動かす)
💡 コツ: int a[3][4] は内部的には 12個の int が連続しているだけ。「2次元」は人間側の見方にすぎない。

2次元・3次元の宣言と初期化

2次元配列

// 3行×4列
int m[3][4] = {
    { 1,  2,  3,  4},
    { 5,  6,  7,  8},
    { 9, 10, 11, 12}
};

printf("%d\n", m[1][2]);  // → 7 (1行目の2列目、0起点)

3次元配列

// 2層×3行×4列(立体的なイメージ)
int cube[2][3][4];

for (int l = 0; l < 2; l++)
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 4; j++)
            cube[l][i][j] = l * 100 + i * 10 + j;
初期化の省略: int m[3][4] = {0}; と書けば全要素0に初期化。int m[][4] = {{1,2,3,4},{5,6,7,8}};外側の次元だけ省略可能(列数は必須)。

sizeof の確認

int m[3][4];
sizeof(m)        // 48  (3 * 4 * 4バイト)
sizeof(m[0])     // 16  (1行分 = 4 * 4バイト)
sizeof(m[0][0])  // 4   (intひとつ)
// 要素数を求めるイディオム:
//   行数 = sizeof(m) / sizeof(m[0])
//   列数 = sizeof(m[0]) / sizeof(m[0][0])

行優先メモリレイアウト(row-major)

Cの2次元配列は1行目が全部、次に2行目、…と連続して並びます。これを「行優先(row-major order)」と呼びます。FortranやMATLABは逆の「列優先」なので注意。

論理的な見た目(3×4)

1
2
3
4
5
6
7
8
9
10
11
12

実際のメモリ上(連続する12個のint)

1
2
3
4
5
6
7
8
9
10
11
12

インデックス変換の公式

m[i][j] と等価: (*((int*)m + i * COLS + j))
あるいは: int *p = (int *)m; p[i*COLS + j]
// 2次元配列を1次元のように辿る
int m[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int *p = (int *)m;
for (int k = 0; k < 12; k++)
    printf("%d ", p[k]);
// → 1 2 3 4 5 6 7 8 9 10 11 12
性能のヒント: 2重ループで走査するときは 外ループを行、内ループを列にすると連続アクセスになりCPUキャッシュに有利。逆にすると遅くなる(特に大きな行列で顕著)。

関数への渡し方 ― 4つのパターン

「多次元配列を関数に渡したい」はCでもっとも混乱する局面の1つ。使える4パターンを整理します。

① 列数を型に固定(C89から使える)

// 列数を必ず書く(行数は省略可)
void print_34(int m[][4], int rows) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < 4; j++)
            printf("%d ", m[i][j]);
}

// 呼び出し
int a[3][4];
print_34(a, 3);
利点: 素直な書き方。欠点: 列数が4以外の配列を渡せない。

② VLA(可変長配列)として受ける ― C99+

// 行数・列数の両方を引数にしてから配列を受ける
void print_any(int rows, int cols, int m[rows][cols]) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            printf("%d ", m[i][j]);
}

int a[3][4], b[2][5];
print_any(3, 4, a);
print_any(2, 5, b);
利点: 任意のサイズを渡せる。添え字記法がそのまま使える。欠点: C99+が必要。古いMSVCでは使えない。

③ フラット配列 + 手動インデックス

// 1次元に "つぶして" 渡す。インデックスは自前で計算
void print_flat(int *m, int rows, int cols) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            printf("%d ", m[i * cols + j]);
}

int a[3][4];
print_flat((int *)a, 3, 4);
利点: どんな環境でも動く。malloc確保とも相性が良い。欠点: m[i][j] の記法が使えず読みづらい。

④ ポインタ配列 (int **) ― ①②と別物!

// 各行の先頭ポインタを並べた配列を渡す
void print_pp(int **m, int rows, int cols) {
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            printf("%d ", m[i][j]);
}

// 呼び出し側で「行ポインタの配列」を組む必要がある
int row0[] = {1,2,3};
int row1[] = {4,5,6};
int *rows[] = {row0, row1};
print_pp(rows, 2, 3);
重要: int a[3][4]int ** にキャストしても動かない。メモリ表現が違う(前者は連続したint、後者は「ポインタの配列」)。混同しやすい最大の罠。

動的確保の2方式

サイズが実行時に決まる多次元配列は malloc で作ります。主に2つのパターンがあります。

方式A: 連続領域(推奨)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int rows = 3, cols = 4;

    // 全要素分をまとめて確保
    int *m = malloc(sizeof(int) * rows * cols);
    if (!m) return 1;

    // 手動インデックス: m[i*cols + j]
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            m[i * cols + j] = i * 10 + j;

    free(m);
    return 0;
}
メリット: malloc 1回で済むので速い&漏れにくい。メモリも連続でキャッシュに優しい。

方式A の上で VLA ポインタで m[i][j] 記法

// 連続領域を確保しつつ、2次元インデックスを使う技
int (*m)[cols] = malloc(sizeof(int[rows][cols]));
m[1][2] = 42;   // 普通の添え字でアクセス可能
free(m);

方式B: 行ポインタ配列(int **

int **m = malloc(sizeof(int *) * rows);    // 行ポインタ配列
for (int i = 0; i < rows; i++)
    m[i] = malloc(sizeof(int) * cols);       // 各行ごとに確保

m[1][2] = 42;   // m[i][j] 形式でアクセス可

// 解放は逆順
for (int i = 0; i < rows; i++) free(m[i]);
free(m);
デメリット: malloc を rows+1 回呼ぶので遅く、メモリも断片化する。行ごとに別の場所に散らばるのでキャッシュ効率が落ちる。通常は方式Aを優先

実用例: 行列積

A (m×k) と B (k×n) の積 C (m×n) を計算する。C[i][j] = Σ A[i][l] * B[l][j]
#include <stdio.h>

#define M 2
#define K 3
#define N 2

void matmul(int A[M][K], int B[K][N], int C[M][N]) {
    for (int i = 0; i < M; i++)
        for (int j = 0; j < N; j++) {
            int sum = 0;
            for (int l = 0; l < K; l++)
                sum += A[i][l] * B[l][j];
            C[i][j] = sum;
        }
}

int main(void) {
    int A[M][K] = {{1,2,3},{4,5,6}};
    int B[K][N] = {{7,8},{9,10},{11,12}};
    int C[M][N];
    matmul(A, B, C);

    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++)
            printf("%4d", C[i][j]);
        putchar('\n');
    }
    // 期待:
    //   58  64
    //  139 154
}
ループ順序の工夫: ijl の順より ilj の順で内積を累積する方が、Bを行方向に連続アクセスするので高速(行優先の恩恵)。大規模行列では数倍の差が出る。

実用例: 画像の左右反転(グレースケール)

画像を2次元配列として扱う一番シンプルな処理。各行を左右に反転する。
#include <stdio.h>

#define W 6
#define H 4

void flip_horizontal(unsigned char img[H][W]) {
    for (int i = 0; i < H; i++)
        for (int j = 0; j < W / 2; j++) {
            unsigned char t = img[i][j];
            img[i][j] = img[i][W - 1 - j];
            img[i][W - 1 - j] = t;
        }
}

int main(void) {
    unsigned char img[H][W] = {
        {10,20,30,40,50,60},
        {11,21,31,41,51,61},
        {12,22,32,42,52,62},
        {13,23,33,43,53,63},
    };
    flip_horizontal(img);
    for (int i = 0; i < H; i++) {
        for (int j = 0; j < W; j++) printf("%3d ", img[i][j]);
        putchar('\n');
    }
}
出力: 60 50 40 30 20 10 61 51 41 31 21 11 62 52 42 32 22 12 63 53 43 33 23 13
発展: RGB画像なら unsigned char img[H][W][3] の3次元配列で扱える。上下反転は img[i] ↔ img[H-1-i] を memcpy で行ごとに入れ替える。

チャレンジ課題

課題1: 転置
int A[3][4] の転置 int B[4][3] を求める関数を書け(行と列を入れ替える)。
課題2: 九九の表
int table[9][9] に九九の表を入れて出力せよ。table[i][j] = (i+1) * (j+1)。
課題3: 動的な行列加算
rows と cols をコマンドライン引数で受け取り、malloc で連続領域を2つ確保、乱数で埋めた後に和を計算して表示するプログラムを書け(方式A)。
課題4: 上下反転
本文の画像左右反転を上下反転に書き換えよ。各行全体を memcpy で入れ替える版も作る(W × sizeof(unsigned char)バイト)。
課題5: 対角成分の和(トレース)
n×n の正方行列を受け取り、対角成分の和を返す関数 int trace(int n, int m[n][n])(VLA引数)を実装せよ。

関連ページ