ソースコードを複数個の関数に分割すれば, プログラムを 3C(正確・明解・簡潔)で高品質な形に作りやすい, ということを以前学習した.
今回はさらに,ポインタを利用した関数呼び出しについても理解しよう.
プログラミング言語の一般論として, 関数やサブルーチンの呼び出しのとき,引数の使用方法には, 次の2種類がある:
仮引数として関数内に新たな変数が用意され, 実引数の値が仮引数の変数にコピーされる.
したがって,呼び出された関数内で仮引数の変数値を変化させても, 実引数の変数値は変化しない. (関数の内部での変数値の変更が関数の外部には影響しない.)
したがって,呼び出された関数内で変数値を変化させると, 実引数の変数値も変化する. (関数の内部での変数値の変更が関数の外部にも影響する.)
C言語の場合,関数呼び出しは, すべて,値渡しとして実行される. しかし,ポインタを引数にすれば, 実質的に,参照渡しと同じことになる.
参照渡しを基本としている言語もある. (例:FORTRAN,BASIC)
データ型によって呼び出し方法を切り替えられる言語もある. (例:C++,Java)
Cの関数呼び出しの基本は値渡しとなっている. たとえば:
void func(int x) // 仮引数の x { ... } main() { int x = 1; func(x); // 実引数の x ... }
このコードの仮引数の x と実引数の x とは, 名前と値は同じであるが, 実体は互いに別物である. (同名・別物の変数 x が2個ある.)
このため,値渡しでは, 呼び出された関数内で仮引数を変化させても, 呼び出した側の実引数は変化しない. たとえば,2つの変数 a,b の値を交換するための関数 swap( ) として, List 1 のように定義してもうまく働かない.
/* 変数 a,b の値を交換する関数 */
void swap(int a, int b)
{
int tmp;
tmp = a;
a = b;
b = tmp;
printf("swap : a = %d , b = %d\n", a, b); // テスト出力:関数内での交換後の値
}
main()
{
int a = 2, b = 9;
printf("main : a = %d , b = %d\n", a, b); // テスト出力:交換前の値
swap(a, b);
printf("main : a = %d , b = %d\n", a, b); // テスト出力:交換後の値
}
ちなみに,変数 tmp は, 一時的(temporary)に値を記録しておくためのものだ. 一時変数なしで,たとえば,a = b; b = a; としても 交換にはならない. この場合,a も b も 同じ値(元の b の値)になってしまう.
また,List 1 では,printf( ) が複数の場所に分散しているが, これは,あくまでも実験的なテスト出力のためだ. ふつうの実用的なプログラムでは, 結果出力のための printf( ) を 一箇所(例:main( ) 内だけとか)にまとめて記述すること.
List 1 の実行中のメモリマップの変化の様子を Fig.1 に示す. main( ) 内の変数 a,b と swap( ) 内の変数 a,b は 「名前は同じだけど実体は別物である」 ということに注意しよう. そのため, swap( ) 内で変数値を書き換えても, main( ) 内の変数値は変わらない.

Cで参照渡しを実現するには, 関数の引数を,変数の実体としてではなく, 変数へのポインタとして定義する. List 2 は,List 1 をそのように修正したものだ. このプログラムを実行して, 変数 a,b の値が うまく交換されることを確認してみよう.
/* 変数値 a,b を交換する関数 */
void swap(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
printf("swap : a = %d , b = %d\n", *a, *b); // テスト出力
}
main()
{
int a = 2, b = 9;
printf("main : a = %d , b = %d\n", a, b); // テスト出力
swap(&a, &b);
printf("main : a = %d , b = %d\n", a, b); // テスト出力
}
まず,呼び出し側(main() 内の swap() の呼び出し)では, 実引数の変数名に & を付けている. つまり,変数の値ではなく,そのアドレスを実引数としている.
そして,呼び出され側(void swap(...) { ... })では, 仮引数の変数名に記号「*」を付けている. つまり, 実引数に与えられたアドレスを受け取るため, ポインタを仮引数としている.
なお,値交換の処理(たとえば *a = *b)での記号「*」は, 間接参照(ポインタの参照先に記録されているデータ)を 処理の対象とすることを意味している.
List 2 の実行中のメモリマップの変化の様子を Fig.2 に示す. swap( ) 内の処理によって, main( ) 内の変数値を書き換えていることがポイントだ. Fig.2 の左右でのメモリマップの変化に注目しよう. また,Fig.1 とも比較しよう.

なお,一見,参照渡しの方が便利に思えてしまい, 何でも参照渡しにしてしまいたくなるかもしれない. しかし,実際には,値渡しで十分な場合の方が多いハズだ. うまく使い分けること. 本当に必要なときだけ参照渡し, 普段は値渡し.
何でも参照渡しでやろうとすると,場合によっては, 呼び出し側で余計な手順が必要になり, ソースコードの可読性が損なわれる.
ちなみに,参照渡しでは,引数が戻り値を兼ねていることになる. これを利用すれば,実質的に複数個の戻り値をもつ関数も定義できる. (return される本当の戻り値は1個または0個だけだが..., 引数を戻り値として代用する.)
関数 swap( ) を利用した意味のあるプログラムの例として, ソーティング(sorting;並べ替え)プログラムを紹介する. アルゴリズムとしては,単純な選択ソート法を利用する.
List 3 は,その不完全なソースコードである. 省略部分(...)については,各自で補完してみよう.
/* 変数値 *a,*b を交換する関数 */
void swap(int *a, int *b)
{
... // *a と *b を交換
// printf(...); // テスト出力...もう,理解したので不要.削除.
}
/* 配列 data の n 個の要素をソートする関数 */
void sort(int *data, int n)
{
int i, j;
for (i = 0; i < n-1; i++) {
for (j = i+1; j < n; j++) {
if (data[i] > data[j]) swap(data + i, data + j);
// ↑ または,if (...) swap(&data[i], &data[j]); でも同じ
}
}
}
/* 配列 data の n 個の要素を表示する関数 */
void print(int *data, int n)
{
...
}
/* 配列 data に値を入力する関数
* m:最大の要素数
* return:入力した要素数
*/
int input(int *data, int m)
{
int i, n;
printf("データの個数(%d 個以内)> ", m);
scanf("%d", &n); // そうか,scanf( ) も実は参照呼び出しだったんだー
printf("%d 個の整数 > ", n);
for (i = 0; i < n; i++) {
scanf("%d", data + i);
// ↑ または,scanf("%d", &data[i]);
}
return (n);
}
main()
{
int data[100];
int n;
n = input(data, 100);
printf("ソート前:\n");
print(data, n);
sort(data, n);
printf("ソート後:\n");
print(data, n);
}
ところで,関数 sort( ) ,print( ),input() は, 配列 data[100] が引数になっている例だが, 実引数が配列名 data となっているのに対して, 仮引数はポインタ *data となっている. このことは,「配列名は配列の先頭アドレスを表わす」という規則 (説明済み) から理解できるハズだ. つまり, 実引数が配列の場合, 自動的に(有無を言わせず), 参照渡しとなる.
というわけで,List 3 の交換関数 swap() についても, 他の関数と同様,データ配列を引数として, 次のように定義しても良い:
void swap(int *data, int i, int j)
{
... // data[i] と data[j] を交換...
}
省略部分のコードを各自で補完してみよう.
配列が引数になる場合, たとえば sort( ) を次のように定義することも可能: (List 3 の定義方法と比較せよ.)
void sort(int data[100]) // 可能だがよろしくない
{
...
}
これだと,この関数は,引数配列が要素数 100 の場合だけにしか使えないことになり, 再利用性が低いので,あまり良い方法ではない. もちろん,ソースコードを書き換えれば要素数を変更できるが, 定数 100 が複数の場所(main() 内,swap() 内,その他) に重複・散在してしまうので,変更作業が面倒になってしまうし, 作業ミスなどで矛盾が発生する可能性が高い.
必ず次のように,要素数を仮引数にせよ:
void sort(int data[], int n) // 要素数を可変に または void sort(int *data, int n)
この方法であれば, 要素数の異なる複数の配列に対して, ひとつの関数を使いまわすことも可能になる:
int data1[100], data2[256]; ... sort(data1, 100); sort(data2, 256);
以前作成した analyzer.c を 適切に関数分割せよ. (機能ごとに 関数を定義する. 引数について,値渡しと参照渡しとを使い分ける. それぞれの関数には機能にふさわしい名前を付けること.)
関数分割の一例:
ヒント:以前の analyzer.c では, 入力データを各瞬間には1個だけしか記録していなかった. 今回は,すべてのデータを配列に記録しておく必要がある. List 3 を参考にしよう. ただし,入力終了には,List 3 のような事前のデータ数入力ではなく, 以前の analyzer.c のように番兵を使うこと.
注意:
余裕のある人は,さらに機能を拡張してみよう.