C言語でポインタのポインタのポインタを使わずに「3次元配列」を動的に確保し、関数に渡して操作する(ただしC99)

教育用の覚書。C99の仕様である関数での可変長配列(variable length array, VLA)の利用法の勉強をした。*1
「古い」C言語では多次元配列を動的に確保し、関数に渡して処理するときに、N次元配列の代わりに(ポインタの)N-1ポインタ*2を使って

double** func ( int n0, int n1, double **a )
のようなインターフェースを作らないと、可変長の多次元データ、たとえば画像のピクセルデータのようなファイルごとにサイズの違うものを扱えなかった*3。これでは(ポインタの)N-2ポインタの分だけメモリを余計に必要とする。
しかし、C99では配列を関数に渡すときに、配列長に関して融通が利くようになっていて、仮引数を宣言文の配列長として書ける…ということを今日、はじめて知った............orz............関数のインターフェースを「fortranのように」書ける。もちろん配列の要素へのアクセスでセグメンテーション違反をしないように、実引数を設定するのはプログラマの責任。
以下は3次元配列を試したときのミニマルコード。手元の環境(Win8.1 + TDM-GCC x86_64-w64-mingw32 4.7.1 / Win7 + TDM-GCC mingw32 4.6.1)でコンパイラを通って実行できたのでメモ。Borland C++ 5.5.1 のような古いコンパイラではエラーになる:

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

double* set (const int N0, const int N1, const int N2, double a[N0][N1][N2]) {
  int i,j,k;
  for(i=0;i<N0;i++){
    for(j=0;j<N1;j++){
      for(k=0;k<N2;k++){
        a[i][j][k]= 1000 + 100 * i + 10 * j + k ;
      }
    }
  }
  return (double*)a;
}

void prt (const int N0, const int N1, const int N2, double a[N0][N1][N2]) {
  int i,j,k;
  for(i=0;i<N0;i++){
    for(j=0;j<N1;j++){
      for(k=0;k<N2;k++){
        printf("%f\n",a[i][j][k]);
      }
    }
  }
}

// 実行はコマンドラインで "a.exe 2 3 4" のように行う
int main ( int argc, char* argv[] ) {
  int l,m,n;
  double *b, *c;

  //3次元配列用のメモリの動的確保
  if(argc<3) return 0;
  l= atoi(argv[1]); m= atoi(argv[2]); n= atoi(argv[3]);
  b= (double*)malloc(sizeof(double)*l*m*n);

  //引数に可変長配列を持つ関数への代入
  //  配列型への型キャストの部分が、多分、最も大事
  c= set(l,m,n,(double(*)[m][n])b);
  prt(l,m,n,(double(*)[m][n])c);

  free(b); b= c= 0;
}

*1:配列の仕様は ISO/IEC 9899:TC2 6.7.5.2

*2:「ぽいんたのぽいんたの…ぽいんた」と読んでね。

*3:これは配列添字演算子の動作が、例えば2次元データでは

a[i][j] を *( *( a + i ) + j ) とほどく
ので、a+i でアドレスを型の( i * 後ろの添字の配列長 )個分スキップする計算をしなくてはいけないが、このスキップ長である後ろの添字の配列長を関数インターフェースの引数に仮引数として書けなかったので、関数インターフェースでの配列サイズの動的な変更がきかなかった。有名なカーニハン、リッチーの教科書には、関数の引数に配列を書くときの書き方が書かれている(とても読みにくいけど)。