引数による情報の受け渡し
ポインタ型の引数
第11章では、自作関数の使い方と作り方を説明しましたが、
ここでは、関数から情報を返す方法として、戻り値を使った方法を説明しました。
戻り値を使って情報を返すのが、もっとも簡単な方法であることは間違いありませんが、
この方法では、常に1つの情報しか返すことができません。
2つ以上の情報を返したい時などは不便です。
そのような場合には、ポインタ型の引数を使って情報を返すことができます。
ポインタ型の引数と言っても、別段特別なことではありません。
単に、
引数の型がポインタ型であるだけ で、普通の引数となんら変わりません。
C言語では、関数へ情報を渡す場合、必ず元の変数の値のコピーを渡します。
この様な方法を値渡しと呼び、元の変数の値が変更されないことが特徴です。
ポインタ型の引数であっても、値のコピーが渡される原則に違いはありません。
それでもポインタ型を使うのは、ポインタ型はアドレスを受け取ることができるからです。
関数を呼び出す時に、すでに存在する変数のアドレスを指定すれば、
呼び出された関数で、受け取った
アドレスをポインタ変数に代入 すれば、
後はポインタ変数を通常変数モードに切り替えて、返す情報を代入できます。
返された情報は、呼び出し側で指定した変数に記憶されていることになります。
次のプログラムは、実際にポインタ型の引数を使って情報を返す例です。
#include <stdio.h>
void func(int* pvalue); /* プロトタイプ宣言 */
int main(void)
{
int value = 10;
printf("&value = %p\n", &value);
func(&value); /* アドレスを渡す */
printf("value = %d\n", value);
return 0;
}
void func(int* pvalue)
{
printf("pvalue = %p\n", pvalue);
*pvalue = 100; /* 通常変数モードに切り替えて代入 */
return;
}
このプログラムの実行結果は、次のようになるかもしれません。
なお、これは LSIC-86 での結果なので、アドレスは2バイトになっています。
&value = 0F68 pvalue = 0F68 value = 100
このプログラムでは、関数を呼び出す時に、変数valueのアドレスを渡しています。
func関数に渡されるのは、あくまでもアドレス値そのもの(今回は0F68)です。
func関数ではそのアドレス値がポインタ変数に代入されているので、
当然、func関数に渡したアドレスと受け取ったアドレスは同じになっています。
ポインタ変数にアドレス値が代入されている場合には、
通常変数モードに切り替えてそのメモリを自由に読み書きできるのだから、
結果として、呼び出された関数から、呼び出し元の変数の中身を書き換えられるわけです。
これまで&付きで呼び出していた関数は、すべて同様の仕組みです。
この使い方が、C言語でのもっともポピュラーなポインタの使い方です。
配列型の引数
これまでは取り扱ってきませんでしたが、配列を引数にすることもできます。
しかし、配列の場合、通常の引数とは異なる性質が多く、扱いにくくなります。
とりあえず、今まで通りの方法で配列型の引数を持つ関数を作ってみます。
引数はint型で要素10の配列とし、配列に代入された値の平均を求める関数を作ります。
今まで通りの方法で実装すると、次の通りになります。
#include <stdio.h>
int getaverage(int data[10]);
int main(void)
{
int average, array[10] = { 15, 78, 98, 15, 98, 85, 17, 35, 42, 15 };
average = getaverage(array);
printf("%d\n", average);
return 0;
}
int getaverage(int data[10])
{
int i, average = 0;
for (i = 0; i < 10; i++) {
average += data[i];
}
return average / 10;
}
このプログラムの実行結果は次の通りになります。
関数内では、配列の要素番号0~9までの値を変数に加算して、
最後にその結果を10で割って平均値を求めています。
この様に、
一見すると 配列も引数として渡せるように見えます。
配列型引数の奇妙な性質
前項では、配列を引数として使う方法を説明しましたが、
この関数は、今までの引数ではあり得なかった、奇妙な性質を持っています。
まず、配列の
要素数は無視 されてしまいます。
次のプログラムは、わざと要素数5の配列を渡してみる例です。
#include <stdio.h>
int getaverage(int data[10]);
int main(void)
{
int average, array[5] = { 15, 98, 98, 17, 42 }; /* 要素数が5 */
average = getaverage(array);
printf("%d\n", average);
return 0;
}
int getaverage(int data[10])
{
int i, average = 0;
for (i = 0; i < 10; i++) {
average += data[i];
}
return average / 10;
}
このプログラムの実行結果は次のようになるかもしれません。
引数の型は10要素になっているにもかかわらず、5個しか要素のない配列が渡せます。
その結果、関数側では強引に10個の要素を処理してしまい、おかしな結果となっています。
さらにおかしな現象として、
関数内で配列の値を変えると呼び出し側まで変化 します。
次のプログラムは、関数内で配列の値を変更してみる例です。
#include <stdio.h>
int getaverage(int data[10]);
int main(void)
{
int average, array[10] = { 15, 78, 98, 15, 98, 85, 17, 35, 42, 15 };
printf("array[3] = %d\n", array[3]);
average = getaverage(array);
printf("array[3] = %d\n", array[3]);
printf("%d\n", average);
return 0;
}
int getaverage(int data[10])
{
int i, average = 0;
for (i = 0; i < 10; i++) {
average += data[i];
}
data[3] = 111; /* 引数の配列の値を変更 */
return average / 10;
}
このプログラムの実行結果は次の通りになります。
array[3] = 15 array[3] = 111 49
今までの引数では、呼び出された関数の中で引数の値を変更しても、
呼び出し元の引数の値が変わることはありませんでしたが、
配列では、なぜか、呼び出し先での変更が呼び出し元に影響しています。
この様なことは、
値渡しではあり得ない ことであるはずです。
アドレスを渡している
前項では、配列型の引数の持つ奇妙な性質を説明しました。
あのような現象は、配列が値渡しされていれば、あり得ないことです。
つまり、逆に言えば、
配列自体は値渡しされていない のです。
しかし、実際に関数に配列を渡して平均値を計算することには成功しています。
つまり、なんらかの形で、配列が渡されていることは間違いのない事実です。
この点について検証するために、少し実験を行ってみましょう。
まず、前項で、配列型の引数では要素数は無視されていることはわかりました。
それならばいっそ、要素数を指定しなければどうなるでしょうか?
つまり、関数を、次のように変更してみるのです。
int getaverage(int data[])
この様に書き換えて実行しても、何の問題もなく動作します。
しかも、プロトタイプ宣言で要素数を指定せずに、
実際の関数の宣言では要素数をつけた場合ですら、何のエラーもでません。
このことからも、
要素数は完全に無視されている ことがわかります。
しかし、要素数を無視して、どうやって配列の値を渡しているのでしょうか?
普通に考えれば、配列を渡す場合、要素数の数だけ値をコピーすることになります。
しかし、要素数を無視している以上、そのような方法は使えません。
ここで、もう1つの実験を行ってみたいと思います。
前項で、呼び出された関数で配列の値を変更すると、呼び出し元まで変化しましたが、
この現象は、ポインタ型の引数を使った時と良く似ています。
つまり、
配列ではなくアドレスを渡している のではないかとも考えられます。
試しに、関数を、次のように変更してみました。
int getaverage(int *data);
驚くべきことに、これでも、何の問題もなく動作しました。
これで、先ほどまでの奇妙な現象の原因がすべて判明しました。
つまり、配列を渡していたのではなく、
配列の先頭のアドレスを渡していた のです。
配列の先頭のアドレスを渡すだけならば、要素数などまったく関係ありません。
また、呼び出された関数での配列は、呼び出し元と同じメモリ領域を指すことになるので、
呼び出された関数で配列の値を変えると、呼び出し元も変更されるのは当然です。
このことについてまとめると、まず、次の3つは
同じ意味の仮引数宣言 です。
ただし、この3つが同じ意味になるのは関数の仮引数宣言の場合のみです。
int getaverage(int data[10]);
int getaverage(int data[]);
int getaverage(int* data);
そして、関数の中では、dataはいずれもポインタ型の変数です。
そして、呼び出し先と呼び出し元ではまったく同じメモリ領域の配列を使うことになります。
この3つが同じ意味だと、どれを使って良いのか迷う人もいるかもしれませんが、 筆者としては、2番目のように要素数を省略した形 を使うことを勧めます。 なぜなら、3番目の宣言は、普通のポインタ型と紛らわしいからです。 2番目の宣言であれば、配列を受け取ることが明示的にわかります。 1番目の宣言は、C言語に慣れた人たちには幼稚な宣言に見えます。
前のページ
次のページ