多次元配列と可変長配列

こんにちは、めのんです!

前回までポインタや配列についていろいろ解説してきました。
今回でいったんその話題については終わりにします。
といっても、ポインタや配列の話題は尽きませんので、これからも折に触れて取り上げていくことになるとは思います。

今回はこれまでに詳しくお話しできなかった多次元配列と可変長配列を一気に解説することにします。

多次元配列

これまで解説してきた配列はすべて1次元でした。
2次元でも3次元でも基本的には1次元配列の延長でしかないのですが、さすがに解説無しでは使えないでしょうし、PHPの多次元配列から別のものを連想してしまうかもしれませんからね。

2次元配列

Cで2次元配列を使うには次のようにします。

#include <stdio.h>

int main(void)
{
  int a[3][4] =
  {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
  };

  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < 4; j++)
    {
      printf("%d\n", a[i][j]);
    }
  }
  return 0;
}

何をやろうとしているのか、ほかの言語でのプログラミング経験があればすぐにわかるんじゃないかなと思います。
上のサンプルコードでは、1次元目が3要素、2次元目が4要素になっていますが、初期化子を見ていただければわかるように、int型4要素の配列が3つ集まってこのようになっています。

宣言がどういう構造になっているのか、わかりやすく丸括弧を使って分解してみると次のようになります。

int (a[3]) [4]

要するに、int[4]型を3要素持つ配列ということです。
配列の要素がint[4]型であることを除けば、今までどおりの1次元の配列と同じですよね。

2次元目の要素数をバラバラにする

ここで気付かれた方もいらっしゃるかもしれませんが、2次元目の要素数は固定になります。
PHPであれば次のようにバラバラの要素数にすることができましたね。

$a = [
  [ 1, 2 ],
  [ 3, 4, 5 ],
  [ 6 ],
];

Cの多次元配列では残念ながらこのようなことはできません。
正確には多次元配列ではないのですが、配列の要素をポインタ型にすることで実現することができます。

int x[] = { 1, 2 };
int y[] = { 3, 4, 5 };
int z[] = { 6 };
int *a[] = { x, y, z };  // 多次元配列のように振る舞う

このようなポインタの配列は多次元配列ではありませんので、sizeof演算子を使う場合など、メモリー配列が連続していることを前提とした処理では振る舞いが変わってきます。
ですが、a[i][j] のように添字演算子を2つ並べてそれぞれの要素にアクセスできるという点では同じです。
実際こういう使い方もよくしますので、あわせて覚えておいていただければと思います。

3次元以上の配列

2次元配列が理解できれば3次元以上の配列でも同じように扱うことができます。

具体的にはこんな感じです。

#include <stdio.h>

int main(void)
{
  int a[2][3][4] =
  {
    {
      { 1, 2, 3, 4 },
      { 5, 6, 7, 8 },
      { 9, 10, 11, 12 },
    },
    {
      { 13, 14, 15, 16 },
      { 17, 18, 19, 20 },
      { 21, 22, 23, 24 },
    },
  };

  for (int i = 0; i < 2; i++)
  {
    for (int j = 0; j < 3; j++)
    {
      for (int k = 0; k < 4; k++)
      {
        printf("%d\n", a[i][j][k]);
      }
    }
  }
  return 0;
}

4次元以上はぜひご自身で試してみてくださいね。

可変長配列

次は可変長配列について解説します。

これまで配列の要素数はずっと定数で指定してきました。
ところが状況によって要素数を変えたくなることはよくありますよね。
そんなときは可変長配列の出番です。

可変長配列の宣言

可変長配列は次のようにして使います。

void func(int n)
{
   int a[n];
   ...
}

上の例ではfunc関数の中で仮引数nの値を要素数として配列aを宣言しています。
可変長配列は要素数がいくつになるのか実行時までわかりませんので初期化子を与えることはできません。
けれども配列の宣言さえできてしまえば、あとは自由にその要素を読み書きすることができます。

可変長配列にできないケース

可変長配列はどこででも宣言できるわけではありません。
「どこででも」と書きましたが、ソースコード上の記述場所が制限されるんではなくて、可変長配列は自動記憶域期間でないとダメなんです。
具体的には、関数の外やstatic指定子付きで宣言した場合は静的記憶域間になりますので、可変長配列を使うことはできません。

int m = 10;
int a[m];            // 関数の外はダメ

void func(int n)
{
   static int b[m];  // 関数の中でもstatic指定子を付けるとダメ
   int c[n];         // これならOK
   ...
}

多次元の可変長配列

多次元配列を可変長配列にすることもできます。
たとえばこんな風にです。

void func(int m, int n)
{
   int a[m][n];
   ...
}

多次元配列に可変長引数が使えることからもわかるように、自動記憶域期間であれば、配列の要素や構造体のフィールドに可変長配列を使うこともできます(構造体についてはのちの回で説明します)。

可変長配列に対するsizeof

可変長配列が無ければsizeof演算子の結果はすべてコンパイル時に決定できていました。
実際、C99より前の規格では可変長配列が無かったので、sizeof演算子の評価結果は必ず定数式(コンパイル時に値が決定される式)になっていました。

ところが、C99で可変長配列が導入されてからはそうはいかなくなりました。
sizeof演算子の評価結果は、可変長配列が絡むと実行時でなければ決定できなくなったんです。

次のような簡単なコードを書いて、ぜひご自身で試してみてください。
実行時にsizeof演算子の評価結果が変わることが確認できるはずです。

#include <stdio.h>

int main(void)
{
  for (int i = 1; i <= 10; i++)
  {
    int a[i];
    printf("%zu\n", sizeof a);
  }
  return 0;
}

関数の仮引数に可変長引数を使う

関数に配列を実引数として渡そうとしても、暗黙の型変換でポインタになってしまいます。
このことは前回解説しました。

ポインタに変わってしまうのであれば、何要素の配列かはチェックされませんのである意味可変長といえば可変長です。
それでもかまわないのですが、可変長引数を使って仮引数の配列の要素数を明示することができます、
こんな感じです。

void func(int n, int a[n])

仮引数のnを使って同じく仮引数の配列aの要素数を指定しています。

ここで、仮引数nの宣言はaより前でなければなりません。
もし、nをあとに宣言してしまうと、ファイル有効範囲で宣言されたnを探しにいってしまいます(そして見つからなければエラーになります)。

また、実際にはこのような書き方をしてもaはポインタになってしまいますのであまり意味がありません。

可変長配列を仮引数の配列の要素数にしたとき、一番効果があるのは多次元配列だと思います。
具体的にはこんな感じです。

void func(int m, int n, int a[m][n])

上のサンプルコードでは、func関数の第2引数が可変長配列になっています。
配列aの1次元目の要素数も2次元目の要素数も引数で渡されたmとnです。

1次元配列の場合、関数の仮引数を可変長配列にしてもほとんど意味がありませんでした。
ところが2次元配列になると途端に意味を持ち出します。

たとえばこんなケースを考えてみてください。

void func(int m, int n, int a[m][n])

この場合、aは確かにポインタに暗黙的に変換されてしまいます。
ところがその型は int[n] 型になります。
つまり、可変長配列の要素数が実行結果に反映されたことを意味します。

3次元目以降についても同じことが言えますので、機会があればうまく使いこなしてみてください。

可変長配列の注意点

可変長配列にはいくつかの注意点があります。
すでに解説した注意点としては、

  • 自動記憶域期間を持たなければならない。
  • sizeof演算子の評価結果が定数式にならない。
  • 関数の仮引数が可変長配列になった場合、1次元であればメモ以上の意味が無い。

といったことがありました。
ここではそれ以外の注意点について解説することにします。

大きな配列は避けるべき

自動記憶域期間のオブジェクトは、多くの処理系ではスタックと呼ばれるメモリ領域に割り付けます。
スタックはそんなに大きな領域ではないので、要素数が大きすぎるとプログラムがクラッシュします。

エラーの判定をすることもできませんので、要素数にはあまり大きな数字が入らないように注意する必要があります。
再帰呼び出しをする場合も、どんどんスタックの使用量が膨らんでいきますので要注意です。

C11以降では可変長配列はオプション

C99では可変長配列は必ずサポートされるべき仕様でした。
ところが、その次の規格であるC11からはオプションになっています。
つまり、必ず可変長引数が使えるとは限らないのです。

処理系が可変長配列をサポートするかどうかは__STDC_NO_VLA__というマクロが定義されているかどうかを調べる必要があります。
このマクロが定義されていれば処理系は可変長配列をサポートしていません。
マクロについては今後の回で解説しますので、今はそういうことがある程度に理解しておいてください。


以上で今回の解説を終わります。
次回のテーマを何にするかはまだ決まっていませんが、できればここまでの内容を踏まえて、文字列に関してさらに踏み込んでみたいと思います。