06 月 19 日(金)1-2h

関数とポインタ

ソースコードを複数個の関数に分割すれば, プログラムを 3C(正確・明解・簡潔)で高品質な形に作りやすい, ということを以前学習した.

今回はさらに,ポインタを利用した関数呼び出しについても理解しよう.


関数の呼び出し方法

プログラミング言語の一般論として, 関数やサブルーチンの呼び出しのとき,引数の使用方法には, 次の2種類がある:

なお,「実引数」とは呼び出し側で与えるデータ(定数や変数の値), 「仮引数」とは呼び出され側でデータを受け取る変数(値を入れる箱) のことだ. 以前の説明を確認しよう.

C言語の場合,関数呼び出しは, すべて,値渡しとして実行される.  しかし,ポインタを引数にすれば, 実質的に,参照渡しと同じことになる.

参照渡しを基本としている言語もある. (例:FORTRAN,BASIC)

データ型によって呼び出し方法を切り替えられる言語もある. (例:C++,Java)


Call by Value

Cの関数呼び出しの基本は値渡しとなっている. たとえば:

void func(int x)	// 仮引数の x
{
	...
}

main()
{
	int x = 1;
	func(x);	// 実引数の x
	...
}

このコードの仮引数の x と実引数の x とは, 名前と値は同じであるが,実体は互いに別物である. (同名・別物の変数 x が 2 個ある.)

なお,名前を別々に変えても,値は同じで,実体は別物になる. つまり,実引数と仮引数の間の名前の違いは,まったく重要ではない. (同じ名前でも,異なる名前でも,どちらでも構わない.)

このため,値渡しでは, 呼び出された関数内で仮引数を変化させても, 呼び出した側の実引数は変化しない. たとえば,2つの変数 ab の値を交換するための関数 swap( ) として, List 1 のように定義してもうまく働かない.

List 1. 関数 swap( ) の定義の失敗例 swap-ng.c
/* 変数 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; としても 交換にはならない. この場合,ab も 同じ値(元の b の値)になってしまう.

また,List 1 では,printf( ) が複数の場所に分散しているが, これは,あくまでも実験的なテスト出力のためだ. ふつうの実用的なプログラムでは, 結果出力のための printf( ) を 一箇所(例:main( ) 内だけとか)にまとめて記述すること.

List 1 の実行中のメモリマップの変化の様子を Fig.1 に示す. main( ) 内の変数 abswap( ) 内の変数 ab は 「名前は同じだけど実体は別物である」 ということに注意しよう. そのため, swap( ) 内で変数値を書き換えても, main( ) 内の変数値は変わらない.

Fig.1 swap-ng.c のメモリマップ
いつものように,Fig.1 のアドレスは「てきとー」です. List 1 を改造し,各変数の実際のアドレスを表示してみるとよい.

Call by Reference

Cで参照渡しを実現するには, 関数の引数を,変数の実体としてではなく, 変数へのポインタとして定義する. List 2 は,List 1 をそのように修正したものだ. このプログラムを実行して, 変数 ab の値が うまく交換されることを確認してみよう.

List 2. 関数 swap( ) の定義の成功例 swap-ok.c
/* 変数値 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() の呼び出し)では, 実引数の変数名に & を付けている. つまり,変数の値ではなく,そのアドレスを実引数としている.

入力関数 scanf( ) の呼び出し方法と同じだ.

そして,呼び出され側(void swap(...) { ... })では, 仮引数の変数名に記号「*」を付けている. つまり, 実引数に与えられたアドレスを受け取るため, ポインタを仮引数としている.

なお,値交換の処理(たとえば *a = *b)での記号「*」は, 間接参照(ポインタの参照先に記録されているデータ)を 処理の対象とすることを意味している.

引数宣言の部分と処理内容の部分とで同じ記号「*」を使っているが, 違う意味をもつので注意しよう. ポインタについて自信がなければ, 以前の説明を読み直そう.

List 2 の実行中のメモリマップの変化の様子を Fig.2 に示す. swap( ) 内の処理によって, main( ) 内の変数値を書き換えていることがポイントだ. Fig.2 の左右でのメモリマップの変化に注目しよう. また,Fig.1 とも比較しよう.

Fig.2 swap-ok.c のメモリマップ

なお,一見,参照渡しの方が便利に思えてしまい, 何でも参照渡しにしてしまいたくなるかもしれない. しかし,実際には,値渡しで十分な場合の方が多いハズだ. うまく使い分けること. 本当に必要なときだけ参照渡し普段は値渡し

何でも参照渡しでやろうとすると,場合によっては, 呼び出し側で余計な手順が必要になり, ソースコードの可読性が損なわれる.

ちなみに,参照渡しでは,引数が戻り値を兼ねていることになる. これを利用すれば,実質的に複数個の戻り値をもつ関数も定義できる. (return される本当の戻り値は1個または0個だけだが..., 引数を戻り値として代用する.)


例:ソーティングプログラム

関数 swap( ) を利用した意味のあるプログラムの例として, ソーティング(sorting;並べ替え)プログラムを紹介する. アルゴリズムとしては,単純な選択ソート法を利用する.

List 3 は,その不完全なソースコードである. 省略部分(...)については,各自で補完してみよう.

List 3. ソーティングプログラム mysort.c
/* 変数値 *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] を交換...
}

省略部分のコードを各自で補完してみよう.

なお,main( ) 内の data は配列名なのでアドレスだが, 他の関数内の *data はポインタ(アドレスを記録する変数)だ. また,配列の要素 data[i] は通常の変数だ. 記号「*」や「&」の使い方について,混同しないこと! 以前の説明を再確認せよ.

補足

配列が引数になる場合, たとえば 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適切に関数分割せよ. (機能毎に 関数を定義する. 引数について,値渡しと参照渡しとを使い分ける. それぞれの関数には機能にふさわしい名前を付けること.)

理想的な分割方針としては,1つの関数には1つの機能だけを与えるとよい. ちなみに,「機能」も「関数」も英語では同じく,"function" という.

関数分割の一例:

これは,あくまでも一例. 「もっと素晴らしい分割方法はないか?」を考えよう.

ヒント:以前の analyzer.c では, 入力データを各瞬間には1個だけしか記録していなかった. 今回は,すべてのデータを配列に記録しておく必要がある. List 3 を参考にしよう. ただし,入力終了には,List 3 のような事前のデータ数入力ではなく, 以前の analyzer.c のように番兵を使うこと.

注意:

余裕のある人は,さらに機能を拡張してみよう.


(c) 2015, yanagawa@kushiro-ct.ac.jp