[←1つ前] [↑質問一覧] [↑記事一覧] [ホームページ]
void foo(int **array) { ... } void bar(void) { int array[10][10]; foo(array); ... }
foo
を次のように修
正すればOKです。
void foo(int array[10][10]) { ... }実際は、このような呼び出しにおいては、最初の要素の個数を省略することが 許されています。従って、次のようにしてもOKです。
void foo(int array[][10]) { ... }「ポインタと配列は同じ」とよく言われますが、この表現は大雑把すぎるので 数々の誤解を生んでいるようです。簡単に理解するには、
a[i]
は*(a + i)
と同じ意味である
と覚えておけばよいと思います。a[i]
と書ける所であれば、
それを*(a + i)
と書くことができるし、逆に*(a + i)
と書ける所であれば、a[i]
と書いてもよいのです。この意味で、
ポインタと配列はかなり親密な関係にあるわけです。
しかし、ポインタ変数と配列変数の実態は異なっています。最も大きな差は、
ポインタ変数はアドレスを格納できるサイズを持つ変数であり、配列変数は配列
の要素として値を入れることのできる変数だが配列そのものを表現する値は定数
であるということです。言いかえれば、int *p;
と定義した場合、
p
には代入することができますが、int a[10];
と定義
した場合、a[0]
やa[1]
には代入することができますが、
a
には代入することはできません。これは、p
と
a
の内部表現が全く異なっているからです。
また、おそらくここが混乱の種なのですが、関数の引数に配列を与えた場合に
は、実際に渡される値はそのアドレスである、という規則になっています。従っ
て、関数の引数として与える最初の要素に関してはポインタと解釈しても構わな
いことになり、次のように書くことができます。
void foo(int (*array)[10]) { ... }(参考: c.l.c FAQ 6.18)
---- List 1 ---- void foo1(int array[4]) { ... } void bar1(void) { int a[4]; foo1(a); ... }この関数
foo1
は、int
の配列で要素が4個あるもの
が渡されることを期待していることになりますが、実際は、関数の引数として配
列が渡される場合には、配列の先頭のアドレスが重要なのであって、配列のサイ
ズは中の処理としては使われません。bar1
では、配列aはFig. 1の
ような実体を持つように定義されます。
---- Fig. 1 ---- +-------+ <-- ここのアドレスが a | a[0] | +-------+ | a[1] | +-------+ | a[2] | +-------+ | a[3] | +-------+
bar1
は、関数foo1
を、foo1(a);
のよ
うにして呼び出しています。この時与えられるa
というのは配列の
先頭の要素を指すアドレスであって、この時点では配列要素の個数のような情報
は全く渡らないことになります。
関数foo1
は、この情報を受け取って処理しようとしますが、一次
元の配列の場合は、先頭のアドレスと、各要素のサイズが分かれば、それぞれの
要素を参照することができます。この場合は要素がint
であり、先
頭の要素のアドレスがarray
であることが分かっていれば、
n番目の要素はarray
からint
をn個格納できるスペー
スを飛ばした先にあることが分かるのです。この場合、元の配列で定義されてい
た要素の個数は必要ありません。従って、関数foo1
は、List 1'
のようにすることもできます。
---- List 1' ---- void foo1(int array[]) { ... }ただし、いずれの場合も、定義された個数を超えて読み書きしようとすると、 何が起こるか分からないので注意が必要です。許された範囲内で処理することは、 プログラム側で保証してやらなければなりません。 次に、二次元の配列を考えてみます。で考えてみましょう。List 2のような引 数で呼び出される関数を想定します。
---- List 2 ---- void foo2(int array[3][4]) { ... } void bar2(void) { int a[3][4]; foo2(a); ... }二次元配列の場合は、実体はFig.2のように取られることになっています。
---- Fig.2 ---- +==========+ <-- ここのアドレスが a | a[0][0] | +----------+ | a[0][1] | +----------+ | a[0][2] | +----------+ | a[0][3] | +==========+ | a[1][0] | +----------+ | a[1][1] | +----------+ | a[1][2] | +----------+ | a[1][3] | +==========+ | a[2][0] | +----------+ | a[2][1] | +----------+ | a[2][2] | +----------+ | a[2][3] | +==========+
a[2][1]
の値を参照する場合、まず、a[2]
という
情報から、データが先頭から3番目(添字が0から始まるのでa[0]
が
1番目、a[1]
が2番目と考える)のブロックにあることがわかります。
1つのブロックは、int a[3][4];
と定義された配列ですから、4つの
int
型の変数が格納できるサイズであることが分かります。
結局、a[2][0]
の値がある場所は、a[0][0]
の位置
に比べて「4つのint
型の変数が格納できるサイズ」を2つ飛ばした
先にあることが分かります。
a[2][0]
の値がある場所が分かれば、a[2][1]
の場
所は、そこからint
型の変数が格納できるサイズ1つを飛ばした先
にあることが分かります。こうやって、プログラムの内部で二重配列を参照する
仕組みになっているのです。
さて、関数foo2
を呼び出す時には、先ほどの例と同じで、やはり
foo2(a)
のように、配列の先頭アドレスしか渡されていません。配
列の要素や、それが二重配列であるというような情報は、この時点では一切知る
よしもない状態です。このような情報は、foo2
が受け取った時に、
foo2
側で考えることになります。すなわち、関数foo2
は、引数として書かれている箇所のint array[3][4]
という情報を
見て、それを判断します。
このように定義されたfoo2
は、与えられた引数が3×4の
int
の配列だと想定して解釈しますが、呼び出した側で本当にその
ような形式のデータを渡したかどうかは関知しません。変な型を与えて呼び出し
ても、あくまで3×4のint
の配列とみなして処理します。その結果
どうなるかは分かりませんので、プログラマーはそのようなことのないように、
関数を呼び出す側と呼び出される側で、引数の型が一致しているようにプログラ
ムを書く必要があります。幸い、ANSI Cの場合は、プロトタイプ宣言を行うこと
により、引数の型の不一致を事前にチェックすることができますから、実行して
破綻するまで失敗に気付かないということは少なくなりました。ただし、先に述
べたように、int a[3]
のような配列を引数に与えた場合に、処理す
る関数がa[3]
以降(a[3]
も含む)を使うことのないよ
うに注意する必要はあります。
ところで、関数foo2
の処理をもう一度考えてみると、
int array[3][4]
という情報のうち、[4]
という情報
は、二重配列における一連のブロックの大きさを計算する時に使っていますが、
[3]
という情報はどこにも使われていません。これは一次元の配列
の場合と同様です。そこで、実は関数foo2
を書く時には、List 2'
のように、先頭要素のサイズを省略することもできます。この情報がなくても、
関数foo2
の内部処理には何の差し支えもないからです。
---- List 2' ---- void foo2(int array[][4]) { ... }これは、一般に「多重配列を関数の引数に与える場合には、呼び出される関数 の仮引数に書く配列の先頭要素サイズを省略することができる」と拡張すること ができます。三重配列の場合は、List 3のようになります。
---- List 3 ---- void foo3(int array[][3][4]) { ... } void bar3(void) { int a[2][3][4]; foo3(a); ... }
malloc
で確
保するにはどのようにすればよいか。
malloc
で動的に確保したメモリを割り当てることができます。
例えば、M×N個のint
の要素を持つ配列を扱うことを考えます。
すなわち、int a[M][N]
のような配列を考えます。この配列を扱う
コードの例は、Listのようになります。
---- List ---- #include <stdio.h> #include <stdlib.h> #define M 5 #define N 10 void foo(int a[M][N]) /* int a[][N] としてもよい */ { int x, y; for (x = 0; x < M; x++) { for (y = 0; y < N; y++) { printf("a[%d][%d] = %d\n", x, y, a[x][y]); } } } int main(void) { int (*a)[N]; int i, j; a = malloc(sizeof(*a) * M); if (a == NULL) { return -1; } for (i = 0; i < M; i++) { for (j = 0; j < N; j++) { a[i][j] = i * 3 + j * 2; } } foo(a); return 0; }(参考: c.l.c FAQ 6.16)
a[M][N]
の領域をmalloc
を使って動
的に獲得したい。ただし、M、Nは実行時まで分からないとする。どのようにすれ
ば実現できるか?
malloc
を使って領域確保することはできません。従って、C言語の
配列をそのまま使うのではなく、工夫が必要になります。
一番単純な方法としては、M×N個の要素が格納できる一次元配列を割り当てて
おき、要素を参照する時にはその都度計算するというものがあります。
---- List ---- int *p; int i, j, m, n; ... p = malloc(sizeof(int) * M * N); /* a[m][n]への代入 */ *(p + (m * N + n)) = i; /* a[m][n]の値の参照 */ j = *(p + (m * N + n));この他に、各ブロック毎に
malloc
してポインタを割り当てる方
法や、無駄を承知であらかじめ十分大きな配列を確保しておく方法などがあります。
char *str = "sample string"; *str = 'S';
NULL
であるかどうかのテストを、
if (p != NULL)
ではなくif (p)
の
ように書いても構わないのか。
p
がポインタであるかどうかにかかわらず、if (p)
と書いた場合には、これがif (p != 0)
と等しい結果となるように
動作します。これが大前提となります。
すると、p
がポインタである場合には、p != 0
とい
う比較の式は、型を一致させるために、右辺に現れる式「0」をNULL
とみなすことになります。なぜなら、0をポインタ型にキャストした
(void *)0
は、NULL
と一致するからです。
従って、結局if (p)
と書いた結果とif (p != NULL)
と書いた結果は常に一致するはずです。これはNULL
の内部表現が0
という値であるかどうかには無関係です。
同様に、if (!p)
と書けば、if (p == NULL)
と同
じ結果が得られます。
これらの書き方のどちらが望ましいかということについては、賛否両論で、決
定的な意見はないようです。私見としては、どちらでも構わないと思います。
---- Fig ---- short int i = 0x1234; アドレス $+0 $+1 +------+------+ ビッグエンディアン | 0x12 | 0x34 | +------+------+ +------+------+ リトルエンディアン | 0x34 | 0x12 | +------+------+ ---- List ---- #include <stdio.h> int main(void) { int i = 1; printf("%s-endian\n", *((char *) &i) == 1 ? "little" : "big"); return 0; }(参考: c.l.c FAQ 20.9)
int foo(int i) { if(0 == i) return func0(); if(1 == i) return func1(); if(2 == i) return func2(); if(3 == i) return func3(); return -1; }
typedef
で定義します。例えば
int
型の関数であれば、typedef int (*int_fp)();
のように決めておきます。こうすれば、プログラムを分かりやすく書くことがで
きます。
あとは、この型の配列を用意して、呼び出す時に添字を使ってやればよいだけ
です。Listでは、function_array[i]
が関数型int_fp
を持ちますので、関数呼び出しを意味するように、()を付けてやればOKです。
---- List ---- typedef int (*int_fp)(); /* int型の関数へのポインタ型 */ int func0(void); int func1(void); int func2(void); int func3(void); int_fp function_array[4] = {func0, func1, func2, func3}; int foo(int i) { if (i >= 0 && i <= 3) { return function_array[i](); } return -1; }
i = 0; j = i++; /* jは0 */ i = 0; j = ++i; /* jは1 */では、次のコードを実行すると、jの値はどうなるのか。
i = 0; i++, j = i;
j = i++;
という代入を詳しく考えてみると、j = 式; という形式
になっていることが分かります。ここで「式」に該当するのは、「i++」です。
C言語の特徴として「式が値を持つ」という性質があることを思い出してくださ
い。インクリメント演算子「++」は、前置しても後置しても、それを作用させる
変数を1増加するというふるまいに関しては同じ結果になります。異なるのは、式
の値です。i++
と書いた場合には、式の値はiに1を加える前の最初
の値に等しくなりますが、++i
とすると、式の値はiに1を加えた後
の値となります。
これに対して、コンマ演算子を使った、i++, j=1;
というコード
を考えてみます。これは「式, 式;」という形式になっています。コンマ演算子
の場合、コンマの左辺の式の値は無視されることになり、右辺には影響しません。
従って、右辺に影響するのは++
演算子によって変更された
i
の値そのものということになります。これがj
に代
入されるのですから、i
の値、すなわち++
演算子によ
って1を加えた後の「1」という値がj
に入ることになります。
コンマ演算子は、コンマの所で評価が完了することになっています。副作用は、
この時点で完了します。その後にコンマの右が評価されるという順序が保証され
ています。従って、次のように書いたコードも結果は同じことになります。
i = 0; ++i, j = i;
※ c.l.c FAQ : comp.lang.c FAQ list http://www.eskimo.com/~scs/C-faq.top.html 文中の項目番号は新しい版に対応しています。旧版とは異なります。