当記事は、全五回シリーズの四回目。
今回は関数に配列を渡す場合にポインタが最適である理由を紹介したい。
C言語入門[4/5] 関数に配列を渡す場合にポインタが最適
ポインタを使う状況はいろんな場合があるが、まず最初にワテが思いつくのは配列を関数に渡す場合だ。
要素数10個の整数型配列aryを宣言して、それに値を入れて関数funcI1()をコール。
#include <stdio.h> int main(int argc, char **argv) { int ary[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int sum = funcI1(ary); getchar(); } int funcI1(int ary[10]){ int sum = 0; for (int i = 0; i < 10; i++){ int ary_i = ary[i]; printf("i=%d, %d\n", i, ary_i); sum += ary_i; } return sum; }
そのfuncI1()では配列の要素を全部足し算してその結果を返す。
そういう単純な処理だ。
この場合には、main()の中で定義された配列ary[10]は、関数funcI1()の引数で渡されると値がコピーされてその複製が作成されるのではなくて、main()の中で定義された配列ary[10]の実体そのものが渡される。
それはつまりary[10]と言う配列が格納されているメモリのアドレスが関数funcI1の引数aryに渡って来るからだ。
C言語では何らかの配列 ary[10] が有れば、その配列の名前 ary はその配列が格納されているメモリの先頭のアドレスを指す。その位置からアドレスが増える方向に配列の要素が並んでいるのだ。
なので、funcI1の中でary[i] の値を変更すると、それはメモリ上のary[i]の値が変更される事になるので、return sum; でmain()に戻って来た時点で配列 aryの中身は変化している。
しかし、
よその関数内で勝手に値を変えられたら困る。
と言う人も居るだろう。
そういう場合には、以下のようにconstを付けて置けば良い。
int funcI1( int const ary[10]) // const を付けておくとaryの中身を変えられない { int sum = 0; for (int i = 0; i < 10; i++){ int ary_i = ary[i]; printf("i=%d, %d\n", i, ary_i); sum += ary_i; // ary[i] = 0; // これを有効化するとエラーする } return sum; }
心配性のワテには有り難い const だ。
このようにしておくと、この関数内でうっかり配列の中身を変更しようとすると文法エラーとなるので事前にそういううっかりミスを発見できる。
ワテの場合、とりあえず変更して欲しくない配列引数の場合には const を入れるようにしている。
配列の大きさが10固定だと使い辛い。
まあ、これで配列を関数に渡す事が出来るので問題なく動くのだが、気になる点がある。
それは、現実的なプログラミングの状況では、毎回固定サイズ10の配列を渡すなどと言う状況は少ない。
通常は、funcI1()をコールする時点で、渡そうとしている配列のサイズが10かも知れないし、100かも知れない。
なので、上記の例のように配列要素数10と仮定した処理を行うfuncI1()の実用性は乏しい。
ではどうするか?
配列の要素数が可変の場合
二つの例を作成してみた。
#include <stdio.h> int main(int argc, char **argv) { int ary[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int sum2 = funcI2(ary, 10); int sum3 = funcI3(ary, 10); getchar(); } int funcI2(int ary[], int N){ int sum = 0; for (int i = 0; i < N; i++){ int ary_i = ary[i]; printf("i=%d, %d\n", i, ary_i); sum += ary_i; } return sum; } int funcI3(int *ary, int N){ int sum = 0; for (int i = 0; i < N; i++){ int ary_i = ary[i]; printf("i=%d, %d\n", i, ary_i); sum += ary_i; } return sum; }
funcI2(), funcI3()ともに、引数が二つあり二番目の引数Nで配列要素数を渡している。
これでこれらの関数は配列要素数が10固定というfuncI1()のようなヘンテコな書き方では無くなったので汎用性が高まった。要素数が何個の配列でもその総和をsumに求める事が出来る。
さて、
int funcI2(int ary[], int N){ // ①
や、
int funcI3(int *ary, int N){ // ②
の部分がポインタ初心者の人には分かり辛いかも知れないが、そんなに難しく考える必要は無い。
どちらの関数も呼び出し元では、
funcI2(ary, 10); funcI3(ary, 10);
の形式であり、配列名aryを渡している。
C言語ではこのように配列名aryを[]無しで単体で用いた場合には、その意味は、その配列のメモリ上の先頭のアドレスを指す。
その様子をもう少し詳しく見てみよう。
メモリーマップ
C言語の場合、配列 int ary[10]; と宣言した場合にはその配列要素10個を保管する領域がメモリ上に連続した領域として確保される。
Visual Studioではそのメモリの様子も確認出来る。
0x0035FA88 cc cc cc cc cc cc cc cc
0x0035FA90 00 00 00 00 01 00 00 00
0x0035FA98 02 00 00 00 03 00 00 00
0x0035FAA0 04 00 00 00 05 00 00 00
0x0035FAA8 06 00 00 00 07 00 00 00
0x0035FAB0 08 00 00 00 09 00 00 00
0x0035FAB8 cc cc cc cc cc cc cc cc
この例では、アドレス
0x0035FA90 ~ 0x0035FAB7
の40バイトの領域に確保されている事が分かる。
なお、このアドレス自体はたまたま今回の実行時にはこの値になっただけであり、もう一回実行すると別のアドレスに確保される可能性もある。また、cc というデータが入っている領域は未使用領域なので、このあと別の配列などを宣言した場合にこのccの領域に確保される可能性もある。
また、正確に言うと、C言語のint型は32bit=4バイトなので、上図よりも下図のほうが正確だ。
0x0035FA88 cc cc cc cc cc cc cc cc
0x0035FA90 00 00 00 00 01 00 00 00
0x0035FA98 02 00 00 00 03 00 00 00
0x0035FAA0 04 00 00 00 05 00 00 00
0x0035FAA8 06 00 00 00 07 00 00 00
0x0035FAB0 08 00 00 00 09 00 00 00
0x0035FAB8 cc cc cc cc cc cc cc cc
つまり、ary[0]の要素は
0x0035FA90 0x0035FA91 0x0035FA92 0x0035FA93
まで連続する4バイトのアドレスに
00 00 00 00
と格納されている。
つまり上図の薄緑でグループ化しているように四つのアドレス(=4バイト)を使って配列要素一個分の情報を記憶している。
同様に、
ary[1]はその隣に、
0x0035FA94 0x0035FA95 0x0035FA96 0x0035FA97
まで連続する4バイトのアドレスに
01 00 00 00
と格納されている。
なので、説明が長くなったが、
ary
という配列名は 0x0035FA90 というアドレスを指すのだ。
つまりaryは、配列 int ary[10]; で確保した配列の先頭要素ary[0](=4バイト)のその先頭の番地(=アドレス)だ。
ちなみに上図のようにメモリのアドレスのどこに何が保管されているかなどの情報を一覧表示したものをメモリマップなどと言う。
ところで
一つ補足だが、
int funcI2(int ary[], int N){ // ①
や、
int funcI3(int *ary, int N){ // ②
の形式で引数 ary の配列を受け取った場合には、それはary配列のアドレスが来るので、これらの関数内でary[i]の値を変更すれば呼び出し元のmain関数側のary[i]の中身も変化すると言う事だった。
一方、int N に関しては、そうはならない。
あくまでNの値が渡って来るだけなので、この関数内でNの値を変えても、呼び出し元のmain関数内には影響しない。
でも、呼び出し元にもNの変更を反映させたいと言う人も居るだろう。
その場合にはいろんな手法があるが、その一つにポインタを使う方法もある。
その辺りは続きで説明したい。
続く
コメント