関数3(参照呼出)

ポインタによるデータ共有について理解しよう. (まぁ,大抵の用途のプログラムは, ポインタなんて使わなくても作成できますが... ポインタを有効に利用すれば, メモリ使用量・実行時間が節約された より効率的なプログラムを実現できるようになる.)

教科書の該当範囲:第9章,第14章

間接参照

基礎知識

使用例:

int	*p;	// ポインタ変数 p の宣言.int型データの位置を指すよ
int	x = 1, y = 2;

p = &x;		// x のアドレスを p に代入
y = *p;		// 間接的に x を読み取る.y = x; と同じ
*p = 0;		// 間接的に x へ書き込む.x = 0; と同じ

// 結果的に,x = 0, y = 1 となったハズ
実験

ポインタ経由の間接参照について実験してみよう. 実験用プログラム indirect.c

#include <stdio.h>

int main(void)
{
	int	x = 1;
	int	y = 2;
	int	*p;		// 変数p はポインタだよ

	// データの確認
	printf("&x = %lx, x = %d\n", &x, x);	// x のアドレスと値を確認
	printf("&y = %lx, y = %d\n", &y, y);	// y のアドレスと値を確認
	printf("\n");

	// x の間接参照
	p = &x;		// ポインタp に変数x を参照させたよ
	printf("p = %lx, *p = %d\n", p, *p);	// 参照先の x のアドレスと値になるよ
	*p = 128;				// 間接参照で x に128が代入されるよ
	printf("p = %lx, *p = %d\n", p, *p);	// 参照先の x のアドレスと値になるよ

	// データの確認
	printf("&x = %lx, x = %d\n", &x, x);	// x のアドレスと値を確認
	printf("&y = %lx, y = %d\n", &y, y);	// y のアドレスと値を確認
	printf("\n");

	// y の間接参照
	p = &y;		// ポインタp に変数y を参照させたよ
	printf("p = %lx, *p = %d\n", p, *p);	// 参照先の y のアドレスと値になるよ
	*p = 256;				// 間接参照で y に256が代入されるよ.
	printf("p = %lx, *p = %d\n", p, *p);	// 参照先の y のアドレスと値になるよ

	// データの確認
	printf("&x = %lx, x = %d\n", &x, x);	// x のアドレスと値を確認
	printf("&y = %lx, y = %d\n", &y, y);	// y のアドレスと値を確認
	printf("\n");

	return (0);
}
実験室の PC では,メモリアドレスが 64 bit(8 byte 整数)なので, printf() の変換指定子として「%lx」を利用しました. (「%lx」だとコンパイル時に警告 Warning が発生しますね. 「%p」でも良いようですが, printf() の行が p だらけになります.) なお,アドレスを表示するなんてことは,あくまでも実験のためだけですよ. 通常の実用的なブログラムでは,ほとんどありえないでしょう.

indirect 実行中のメモリ内容の変化

ポインタ経由で他の変数を操作できることを理解できたかな?


参照呼び出し

基礎知識

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

C言語の場合,関数呼び出しはすべて値渡しとして実行される. しかし,ポインタを引数にすれば,実質的に参照渡しと同じことになる. また,関数の戻り値は1個以内であるが, 参照渡しによって,引数を第2,第3の戻り値としても利用可能となる.

実引数,仮引数,戻り値などについて,以前の説明を再確認しよう.
実験

変数値を書き換えるような関数を作成し, 値渡しと参照渡しの動作の違いを確認してみよう.

まず,値渡し版の失敗例を試してみよう. reset.c

#include <stdio.h>

/*
引数 x の値をゼロにする関数
(値渡し版,失敗例)
*/
void reset(int x)
{
	printf("sub():1: x = %d\n", x);
	x = 0;		// 変数x の値をゼロにする
	printf("sub():2: x = %d\n", x);
}

int main(void)
{
	int	x = 123;

	printf("main():1: x = %d\n", x);
	reset(x);	// 変数x の値による呼び出し
	printf("main():2: x = %d\n", x);
	return (0);
}

実行結果:

main():1: x = 123
sub():1: x = 123
sub():2: x = 0		# sub()ではリセットできたが...
main():2: x = 123	# main()に戻るとできてない
main() 内で reset(x) を呼び出すと → 実引数 x の分身(仮引数の x)が作られ → reset() 内での x = 0 は仮引数の方だけを変更する. 実引数の方は変更されない.

reset(値渡し版)の実行中のメモリ内容の変化

次に,関数を参照渡し版に書き換えてみよう.

/*
引数 *p の値をゼロにする関数
(参照渡し版,成功例)
*/
void reset(int *p)	// ポインタp でアドレスを受け取る
{
	printf("sub():1: *p = %d\n", *p);
	*p = 0;		// ポインタp の参照先の値をゼロにする
	printf("sub():2: *p = %d\n", *p);
}

int main(void)
{
	int	x = 123;

	printf("main():1: x = %d\n", x);
	reset(&x);	// 変数x の参照による呼び出し
	printf("main():2: x = %d\n", x);
	return (0);
}

実行結果:

main():1: x = 123
sub():1: *p = 123
sub():2: *p = 0		# sub()ではリセットできた
main():2: x = 0		# main()でもリセットできてた
main() 内で reset(&x) を呼び出すと → 引数がコピー p = &x され → reset() 内での *p = 0 は x = 0 となる.

reset(参照渡し版)の実行中のメモリ内容の変化

ちなみに,よく利用してきた入力関数 scanf() も 実は参照渡しの関数である. たとえば,scanf("%d", &x); として呼び出すと, scanf()内で入力された数値が 呼出元の変数 x と同じメモリ領域に格納される.


配列と関数

配列データを引数として関数を呼び出すこともできる. ただし,C言語の配列は,次のような性質をもつ:

したがって,配列を引数とした場合,必ず参照渡しとなり, 配列のメモリ領域は呼出元と呼出先の関数の間で共有される. 実引数は配列名(先頭アドレス),仮引数はポインタとし, さらに,配列の要素数も引数として適切に利用し, バッファオーバラン等を防止する必要がある.

配列引数の適切な使用例:set.c

#include <stdio.h>

/*
配列要素へ安全に数値を代入する関数
引数 p:配列の先頭アドレス
     n:配列の要素数
     i:要素番号
     v:数値
戻り値:エラーコード(0:正常,1:エラー発生)
*/
int set(int *p, int n, int i, int v)
{
	if (i < 0) return (1);		// バッファオーバラン防止
	if (i >= n) return (1);		//    〃

	p[i] = v;
	return (0);
}

int main(void)
{
	int	a[10];

	printf("%d\n", a[8]);

	set(a, 10, 8, 888);	// a[8] = 888; と同じですが...
	set(a, 10, -100, 0);	// a[-100] = 0; だとオーバランで強制終了かも
	set(a, 10, 10000, 0);	// a[10000] = 0; でもオーバランかも

	printf("%d\n", a[8]);	// オーバランを発生させず,無事に終了

	return (0);
}
配列引数の仮引数について, ポインタではなく配列として記述する流儀もある. 例:void set(int p[], ...)

本日の課題

次のような配列操作関数を定義せよ:

なお,引数の p および q は配列の先頭アドレスのポインタ, n は配列の要素数, v は初期値である.

テスト用ソースファイル断片 array.c

#include <stdio.h>
#define	NUM	5

...

int main(void)
{
	int     a[NUM], b[NUM];

	print(a, NUM);		// ゴミが表示される

	reset(a, NUM, 7);	// a の全要素を 7 にする
	print(a, NUM);		// 7 7 7 ... が表示される

	input(a, NUM);		// てきとーなデータを a にキーボード入力すると...
	print(a, NUM);		// そいつらが表示される

	copy(a, b, NUM);	// a の全要素を b にコピーする
	print(b, NUM);		// a と同じ内容が表示される

	return (0);
}

余裕ある人は,他の配列操作関数を考案・実装してみては? 合計・平均・最大・最小などの計算とか.

提出:


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