C言語基礎|多次元配列引数の宣言規則

多次元配列って、見た目は「表」や「箱」みたいで直感的なんですが、関数に渡す瞬間にルールが一気に厳しくなるのがC言語の面白いところです。

ポイントはこれです。

  • 一番外側(最も高い次元)の要素数だけは省略できる(=可変にしやすい)
  • それより内側(n−1次元以下)の要素数は、原則として関数側で分かる形で宣言する必要がある

この「内側サイズが必要」という縛りがあるおかげで、コンパイラは m[i][j] の位置(アドレス)を正しく計算できるようになります。

結論:仮引数の宣言ルール(n次元配列)

n次元の多次元配列を受け取る関数の仮引数は、次のルールで宣言します。

  • n 次元(最も外側)の要素数は省略可能
    (書いても実質的に使われないので、別の引数で受け取るのが基本)
  • (n − 1) 次元以下の要素数は、関数側で確定できるように宣言する
    (典型的には定数で書く)

1次元〜3次元の典型宣言

次元典型的な仮引数宣言何が可変?要素型の見え方
1次元void func1(int v[], int n);n(要素数)要素は int
2次元void func2(int v[][3], int n);n(行数)要素は int[3]
3次元void func3(int v[][2][3], int n);n(一番外側の個数)要素は int[2][3]

ここで大事なのは、2次元配列 v[][3] は「int の配列」ではなく、int[3] を要素にもつ配列として扱われる点です。
つまり「1要素=行まるごと」みたいなイメージになります。

どうして内側の要素数が必要なの?

2次元配列 m[i][j] を考えると、i 行目に移動するには

  • 1行が何個(何バイト)なのか

が分からないと計算できません。

図:2次元配列は「行サイズ」が道しるべ

m[i][j] の位置
= 先頭 + (i 行ぶんのサイズ) + (j 要素ぶん)

行ぶんのサイズ = 列数 × 1要素のサイズ

だから関数側で m[][列数] の 列数が必要になります。
一方、行数は「何行まで処理するか」を別引数で渡せばよいので、省略しやすいわけです。

サンプルプログラム

  • 4週(列数)は固定
  • 店舗数(行数)は可変(2店舗でも5店舗でもOK)

という設定にすると、宣言ルールがハマりやすいです。

例:n店舗×4週の売上表を、同じ値で埋めて表示

プロジェクト名:chap6-19-1 ソースファイル名:chap6-19-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

// n店舗×4週の売上表を同じ値で埋めて表示

#include <stdio.h>

#define WEEKS 4

//--- 列数が4の2次元配列 sales の全要素に value を代入 ---//
void fill_sales(int sales[][WEEKS], int stores, int value)
{
    for (int i = 0; i < stores; i++)
        for (int w = 0; w < WEEKS; w++)
            sales[i][w] = value;
}

//--- 列数が4の2次元配列 sales を表示 ---//
void print_sales(const int sales[][WEEKS], int stores)
{
    for (int i = 0; i < stores; i++) {
        for (int w = 0; w < WEEKS; w++)
            printf("%6d", sales[i][w]);
        putchar('\n');
    }
}

int main(void)
{
    int value;

    int shopA[2][WEEKS] = {0};   // 2店舗×4週
    int shopB[5][WEEKS] = {0};   // 5店舗×4週

    printf("全ての週の売上に入れる仮の金額:");
    scanf("%d", &value);

    fill_sales(shopA, 2, value);
    fill_sales(shopB, 5, value);

    puts("shopA(2店舗×4週)");
    print_sales(shopA, 2);

    puts("shopB(5店舗×4週)");
    print_sales(shopB, 5);

    return 0;
}

このプログラムが「宣言規則」をどう使っているか

図:関数に渡しているのは「列数4の表」

main
  shopA(2×4) ─┐
                  ├─> fill_sales / print_sales (列数4が前提)
  shopB(5×4) ─┘

fill_sales と print_sales の仮引数は sales[][WEEKS] です。
ここで 列数が4であること(WEEKSが4) が関数側で分かっているので、sales[i][w] の計算が成立します。

そして行数(店舗数)は、

  • stores という別引数で渡している

ので、2店舗でも5店舗でも同じ関数が使えます。

const を付ける判断(読み取り専用の安心)

今回の print_sales は表示するだけで、配列の中身を書き換えません。
その場合は const を付けて「書き換え禁止」にしておくと安全です。

const を付けるかチェック

関数引数中身を書き換える?宣言の形
fill_salessalesはいint sales[][WEEKS]
print_salessalesいいえconst int sales[][WEEKS]

使った命令(書式と役割)

今回出てきた命令まとめ

命令書式何をする命令?
#include#include <stdio.h>printf / scanf / puts / putchar などの宣言を取り込む
#define#define WEEKS 4マクロ定数を定義する(列数固定に使う)
forfor (初期化; 条件; 更新) 文繰り返し(全要素をなめる2重ループ)
printfprintf(書式, …);書式付き表示
scanfscanf(書式, アドレス);キーボード入力を変数へ読み込む
putsputs(文字列);文字列を表示して改行
putcharputchar(文字);1文字表示(ここでは改行)
constconst 型読み取り専用にする(書き換えミス防止)

もう一歩だけ:C99以降なら「内側サイズも引数で受け取れる」

ここまでの「内側は定数で」という説明は、まず理解しやすい基本形です。
ただ、C99以降では 可変長配列(VLA) を使って、列数を引数で受け取る書き方もできます。

例(考え方だけ)

  • 先に cols を受け取る
  • その cols を使って int m[][cols] と宣言する

このやり方は便利ですが、教材の最初は「列数は定数で固定」から入ると混乱しません。