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 * 後ろの添字の配列長 )個分スキップする計算をしなくてはいけないが、このスキップ長である後ろの添字の配列長を関数インターフェースの引数に仮引数として書けなかったので、関数インターフェースでの配列サイズの動的な変更がきかなかった。有名なカーニハン、リッチーの教科書には、関数の引数に配列を書くときの書き方が書かれている(とても読みにくいけど)。