15.6.1 ポインタ型の引数
これまでの解説では、関数の引数で渡したい値を呼び出し関数に渡して、関数内で処理した結果を戻り値に設定して戻す方法をご紹介しました。
ポインタ変数のよくある使い方は、関数の引数にポインタ変数を渡して、戻り値に設定をせずに、関数内で直接ポインタ変数の指し示すアドレスへ、処理結果を入れていく処理です。
この利点は、戻り値は1つの値しか返せませんが、ポインタ変数を配列で返すことで複数の処理結果を呼び出し元に返すことができます。
また、関数の引数も複数渡したい場合にポインタ変数を一つ渡せば、呼び出し元で格納された値を関数内で取り出すことが可能になります。
このように、渡したい値がたくさんある。
処理結果をたくさん返したい。
といった処理に活躍するのがポインタ変数になります。
まずは、配列のポインタを説明する前に、一つだけの値を関数に渡す方法を見てみましょう。
#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 0;
}
main関数内で宣言されたvalueという変数に、10を初期値として代入しています。
その後、func()関数の引数として、value変数のアドレスを渡しています。
func()関数内では、main関数から渡されたvalueのアドレスを、
*pvalueという変数名で、ポインタ変数として扱っています。
ポインタ変数は、* を付けると格納された値を扱い、* を付けない場合はアドレス値を扱う。
これをモードの変更処理として説明してきました。
ここでは、 * が付いていますので、*pvalue = 100; とすることで、
*pvalueには、アドレスではなく値として100が代入されます。
元々*pvalueはmain関数から value変数のアドレスが設定されて渡ってきていましたので、
*pvalueに設定された値は、main関数のvalueのアドレスに値がセットされます。
つまり、value = 100; とした場合と同じことになります。
これがポインタの基本的な使い方です。
ここでは一つだけの値しか渡していませんので、ポインタを使わない場合と差がないのでは?
と感じるかもしれませんが、配列にするとその差は歴然となります。
配列型の引数
参考書のコードを例にみてみましょう。
#include <stdio.h>
int getaverage(int data[10]);
int main(void)
{
int average;
int 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;
int average = 0;
for(i = 0; i < 10; i++)
{
average += data[i];
}
return average / 10;
}
ポインタと異なる印象に思われるかもしれませんが、配列を引数として関数に渡します。
int型の10個の要素がある配列を引数として関数宣言をして、実際に10個の要素があるarray[10]を渡すことで、配列内の値を全て加算して、10で割った値を戻り値にしています。
ここまでは違和感なく理解ができると思いますが、以下の様に要素数を変えて引数に渡すとどうでしょうか?
#include <stdio.h>
int getaverage(int data[10]);
int main(void)
{
int average;
int array[5] = {15,98,98,17,42};
average = getaverage(array);
printf("%d\n",average);
return 0;
}
int getaverage(int data[10])
{
int i;
int average = 0;
for(i = 0; i < 10; i++)
{
average += data[i];
}
return average / 10;
}
main関数で用意された配列は5個の要素数です。
getaverage関数は、10個の要素を渡すことで宣言されています。
また、関数内の処理は10個の要素を加算するfor文が書かれています。
これはコンパイラでエラーになりそうな感じもしますが、エラーにはなりません。
また、そのまま処理も進めてしまいます。
参考書では、演算の結果が202380394となっていますが、各パソコンで結果は違うものとなります。
これは、5個の要素{15,98,98,17,42}は指定値通りに加算されます。
残りの5個の要素となる不定値が加算されていきます。
{15,98,98,17,42,不定値,不定値,不定値,不定値,不定値}
どの様な値が格納されているかわからないので、各パソコンでの結果は違うものになることを記載しました。
次に、先ほどのプログラムに対して、関数内で別の値を入れてみましょう。
#include <stdio.h>
int getaverage(int data[10]);
int main(void)
{
int average;
int 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;
int average = 0;
for(i = 0; i < 10; i++)
{
average += data[i];
}
data[3] = 111; /* 引数の配列の値を変更 */
return average / 10;
}
関数内で data[3] = 111; とすると、data[3]には15が格納されていたはずですが、111に変化しています。
関数はコピーが引数として渡されて、関数内では値を代入しても変化しないはずですが、配列を引数にして渡した場合は、関数内で代入した値が呼び出し元に処理が帰っても格納されています。
つまり、配列を引数にした場合、以下の2点が確認できました。
①要素数は無視されている
②関数内で引数に値が反映できる
通常の関数のルールとは異なる処理結果です。
これは、配列を引数に渡した場合、以下の処理になっています。
・値のコピーではなく、アドレスが渡されている。
アドレスは配列の先頭要素のアドレスが渡されている
先頭のアドレスが渡されて、関数内でそのアドレスに値を代入することができるというのは、ポインタの処理と同じと言えます。
参考書にもありますが、ポインタの処理として書き換えることができるということです。
int getaverage(int data[10]); ⇒ 上記のコード例
int getaverage(int data[]); ⇒ 要素数を指定しない
int getaverage(int *data); ⇒ ポインタ変数で先頭のアドレスを渡す
これらは何れも同じ結果が得られます。
巷のプログラムでは、関数の引数に配列をそのまま渡すことはしません。
その理由は、用意された要素数の範囲が処理されていると思いたいが、その保証はありません。
先ほどの通り、要素数以内で処理が完了しているかもしれませんし、要素数以上に処理をしてバグになっているかもしれません。
また、配列内の要素が書き換えられてしまうかもしれません。
一般的に、配列要素を書き換える処理、書き換えない処理は、書き換えする関数や場所は限定して処理をするものですが、呼び出した関数内で書き換えられているかどうかが分からない場合、処理を経ると値が変わってしまうことで、想定外のバグに遭遇します。
これらは、明示的にポインタ変数として処理をし、処理範囲であるサイズ、 * を付ける/付けないで、書き換えするか/しないか、アドレス値なのか通常の値なのかを明示することで、可読性や思想を明確にすることが目的となります。