2009年2月24日火曜日

第三回:ポインタ(2)実用編1-関数とポインタ

 ポインタが実際的にどのような機能なのかという点に関しては、第二回で大体説明できたと思う。第三回以降では、ポインタを実際にどのように使うことが出来るか、またはその際、メモリ内はどのような状態になっているのかを説明していこうと思う。第三回は特にポインタがよく使われる場面である、関数の引数としてのポインタの利用の説明を行う。

1.序章


 ポインタがよく使われる場面として、関数とのやりとりが上げられる。文字列の取得を行ったり、関数にデータを渡したりする際にも度々ポインタを使用することになるだろう。ただ、関数とのインタフェースとしてポインタを使用したときに、何故かは分からないがうまく行かないと言う状況に直面することもある。こういった状況を理解するためにも、関数の引数にポインタを使用した後、どのような動きになるかを理解しておくことは重要である。

2.関数への値渡しとポインタ渡し


 C言語の場合、関数への変数の渡し方には値渡しとポインタ渡しと言う2種類の方法がある(C++ではこれに加えて参照渡しなるものもある)。
 2つの引数の渡し方の説明をする場合、一般的な説明としては、値渡しは「関数内でなされた変数への変更を呼出元に反映させない場合」に使用し、ポインタ渡しは「関数内でなされた変数への変更を呼出元に反映させる」場合に使用する。と言うものだろう。
 この説明は正しいが、「何故?」と言う部分が抜けており、本質的な説明を行っていないため、ポインタの分かりにくさを増す結果になってしまう。第二回で説明したポインタの本質が理解できていれば、上記の関数への引数の渡し方に関しする説明は理解しやすいものとなる。

 まずは、以下の2つのソースを見ていただきたい。


/* test1.c */

#include

void SetData( int a , int b );

int main()
{
   int a = 0 , b = 0;

   printf( "a = %d , b = %d\n" , a , b );
   SetData( a , b );
   printf( "a = %d , b = %d\n" , a , b );

   return 0;
}

void SetData( int a , int b )
{
   a = 10;
   b = 20;
}




/* test2.c */

#include

void SetData( int *pa , int *pb );

int main()
{
   int a = 0 , b = 0;

   printf( "a = %d , b = %d\n" , a , b );
   SetData( &a , &b );
   printf( "a = %d , b = %d\n" , a , b );

   return 0;
}

void SetData( int *pa , int *pb )
{
   *pa = 10;
   *pb = 20;
}



 実行すると以下の通り表示される


test1.cの実行結果
a = 0 , b = 0
a = 0 , b = 0




test2.cの実行結果
a = 0 , b = 0
a = 10 , b = 20



 test1.cは関数に対して変数を値渡ししたもの、test2.cは関数に対してポインタ渡ししたものである。test2.cはmain関数内の変数aおよびbがSetDataでセットされた値に変更されていることが分かる。
 何故このような動きになるのだろうか?
 普段プログラマーが意識することはないだろうが、関数を呼び出した場合にはコンピュータは以下のような動きをする。

1.呼び出された関数が使用する領域(スタック領域またはコールスタック)を確保
2.引数として渡された変数の格納領域をスタック領域内に確保
3.引数として渡された変数の値を②で確保した領域にコピー

 この動きは、引数が値渡しであろうが、ポインタ渡しであろうが、変わることはない。3を見て「ん?」と感じた方もおられると思う。値渡しだろうが、ポインタ渡しであろうが、引数領域に値をコピーすると言う動きに変化はないのだ。

 では、上記の1~3の動きをtest1.cおよびtest2.cのソースで説明してみよう。

 まずは、test1.cの説明から。
 main関数を実行すると、main関数のスタック領域が確保される。変数aおよびbは局所変数であるため、このスタック領域内にデータ領域が確保される。変数aおよびbの値は0となる。



 次に、SetDataが引数aおよび、引数bを伴って呼び出される。SetDataは関数用のスタック領域を確保し、int型の二つの変数a,b用の領域をスタック領域内に確保する。次いで、引数で渡された数値をコピーする



 SetData関数内で、a=10;b=20が実行される。



 SetData関数内で変更を行っている変数はmainの変数とは別物であり、そこで行われた変更は当然main関数内の変数に反映されないことが理解できる。

 次に、test2.cを実行した際の、メモリの動きを確認してみよう。
 まず、main関数を実行した際のスタック領域の確保および変数a,bの領域の確保に関しては、test1.cと同様である。
 次に、SetData関数を呼び出すと、SetData関数用のスタック領域が確保され、ここにポインタ変数であるpaおよびpbが確保される。
 mainの中で、


SetData( &a , &b );



 と言う風にSetDataを呼んでいるので、渡される引数はa,bの確保領域のメモリ番地となる。



 SetData関数内では


*pa = 10;
*pb = 20;



 と言う処理が行われる。これは、paのメモリ番地が指す値の中身を10に、pbのメモリ番地が指す値の中身を20にすると言う意味であるから、結果的にmain関数内のaおよびbの中身(値)が書き変わることになる。



 結局は、SetDataに渡している引数(pa、pb)がアドレスであり、pa、pbにではなく、そのアドレスが指し示す先に対して、内容の変更を行っているため、結果的にmain関数の値が書き変わっているだけであり、ポインタ渡しと言っても何か変わったことを行っているのではないことが理解できる。

3.まとめ


 今回は、関数にポインタを渡すときのメモリ内の動きに関して最も単純なケースを用いて説明を行った。今後、配列や構造体のポインタの動きを説明してから、より複雑なポインタ渡しに関しても説明していこうと思う。

0 件のコメント:

コメントを投稿

コメントを承認してから公開しますので即時には反映されません。