第6回:関数とポインタ (10/27)


第5回演習:解答例


これまでの演習で,関数への値の渡し方,結果の戻し方,またポインタ変数を学んできた.

しかし関数で複数の変数の値を変更したり,複数の値(配列など)を渡し処理することは,今までの演習の内容だけでは出来ない.
今回の演習では,これらを組み合わせた技法により以下のことを習得する.

今回の演習の目標


変数のスコープ(有効範囲)

今までに「関数には引数として様々な値を渡すことができる」ことを学んだ.
なぜ異なる関数においては,変数の受け渡しが必要なのだろうか.
それはint型,float型,char型などで宣言される変数は, 宣言する場所によってその有効範囲が異なるためである.

今までの演習で用いて来た変数は,main()をはじめ,ある関数の中で宣言してきた.
このように関数の中で宣言する変数のことをローカル変数という.
ローカル変数を利用できる範囲は,変数を宣言した関数の中,即ち { から } までのブロック内に限られる.
この変数の有効範囲のことを変数のスコープと呼ぶ.
したがって,異なる関数内に「同じ名前」の「異なる変数」を用いる事が可能である.

実際に以下のプログラムでローカル変数間の値が変わらないことを確認してみる.


#include <stdio.h>
 
int funca(void)
{
	int x;      /*この変数は main 内の x とは別物*/
	x = 3;
	
	x += 5;
	
	printf("xの値:%d¥n",x);	  /* funca内のxの値の表示 */
 
	return 0;
}
 
void main()
{
	int x;      /*  この変数は funca 内の x とは別物  */
	x = 3;
	
	printf("xの値:%d¥n",x); /*  main内のxの値の表示  */
	
	funca();
	
	printf("xの値:%d¥n",x);	/*  funca実行後のxの値の表示  */
 
}
 

同じ名前の変数であっても,関数が違えば影響しないことが実行結果からわかる.


値参照とアドレス参照

変数のスコープのため,関数に引数として値を渡さなければ,他の関数の値を用いた計算が出来ない.
引数として用いる変数には,前回の演習で学んだ通常の変数と,ポインタ変数がある.
これらをうまく使い分けることで,様々な関数の作成,利用が可能となる.

値参照(Call by value)

原則:関数の呼び出しでは,呼び出し元から関数へ変数の値がコピー,複製されて渡される.
従って,普通の変数を関数に渡して,関数内で変更しても,関数を呼びだした側の元の変数は変化しない.

これを値参照(Call by value),値渡しと呼ぶ.

呼び出し元の関数に計算結果などの数値を戻したい場合は,return 文を用いる.
このため,戻り値の個数は 0,または 1 のいずれかであった.

簡単なプログラムで確認してみよう.


#include <stdio.h>
 
int test(int a) 
{
    a += 10;  /* この関数内で値を変更 */
 
    return a;
}
 
void main()
{
    int a, b;
 
    printf("aの値を入力してください:");
    scanf("%d", &a);
 
    printf("関数へ渡す前の a の値は,%dです¥n", a);
 
    b = test(a);
 
    printf("関数から戻ると,a の値は %d , 戻り値 b の値は %d ¥n", a, b);
}

例えば a に 2 を入力すると,結果はどのように表示されるだろう.



アドレス参照(Call by reference)

値参照では,最大でも 1 つの値しか関数から返すことができなかった.
また,呼び出した関数内で値を変更した場合も,呼び出し元(mainなど)では値は変化しない.
これは,関数には変数の中身が複製され,渡されることを考えると,当然である.

では,上記の値参照では実現できない

を実現するにはどうしたらよいか.

この場合,関数に変数の格納されている「場所」を伝え, その「場所」のデータを関数内で操作するようにすれば,実現できそうである.

変数の格納されている場所,即ちアドレスを扱うためには, ポインタ変数を利用する.

ポインタを用いて,変数の場所を関数に渡したプログラムの例.


#include <stdio.h>
 
int test2(int *p)        /*  変数のアドレスを受け取る(ポインタ変数)   */
{
    *p += 10;             /*  p の指す中身に変更を加えるから,main関数でも変化する  */
 
    return *p;            /*  値を返す  */
}
 
void main()
{
    int a, b;
 
    printf("aの値を入力してください:");
    scanf("%d", &a);
 
    printf("関数へ渡す前の a の値は,%dです¥n", a);
 
    b = test2(&a);        /* 変数aのアドレスを関数に渡す */
 
    printf("関数から戻ると,a の値は %d , 戻り値 b の値は %d ¥n", a, b);
}

上の例と同様,a に 2 を入力すると,今度はどのようになったか.



配列の受け渡し

ポインタを用いて,関数に変数のアドレスを渡せることがわかった.
では,同じ方法を使って,関数に配列を渡してみよう.

以下は,配列を受け渡しする典型的な例である.
配列名には,配列の先頭のアドレスが格納されているので,ポインタで受け取ることができる.

配列を渡す場合は,配列の先頭アドレスだけではなく,配列の要素数を同時に渡すと,どのような要素数の配列が渡されても,関数内でうまく処理ができる.
(アドレスのみだと,受け取った関数内で配列の個数が分からない.)


#include <stdio.h>
 
void zero_init(int *p, int n)       /*  要素数 n 個,配列の先頭のアドレス p を引数とする関数  */
{
    int i;
 
    for(i = 0; i < n; i++) {
        *(p+i) = 0;     /*  p[i] = 0; という書き方も可 */
    }
    
    return;
}
 
void main()
{
    int i;
    int  ary[] = {10, 20, 30, 40, 50};
 
    zero_init(ary, 5);
 
    for(i =0; i < 5; i++){
        printf("ary[%d] = %d¥n", i, ary[i]);
    }
}
 

このプログラムは,配列 ary の先頭から n 個の要素の値を 0 に設定する関数で,配列の初期化などの際に用いる.
関数には,配列の先頭要素のアドレス(ary)が渡されるので,関数側ではポインタ(int *p)で受け取っている.
この例では関数に戻り値が無いが,関数でのデータ変更は,main 関数においても反映されている.


まとめ

・関数への引数の渡し方として,「値参照」と「アドレス参照」がある.

・値参照では変数の値が(複製されて)渡されるので,関数内で値を変更しても,元の関数には影響が及ばない.戻り値により値を1つだけ戻すことができる.

・アドレス参照では変数のアドレスが(複製されて)渡されるが,関数でポインタの指す値を変更すると,呼び出し元の値も影響される.

・どちらも関数に数値を渡すという点では,本質的に同じ.(アドレス参照ではアドレスという整数値がコピーされ,ポインタ変数に渡される.)


演習課題

(1)変数 a と b の値を,入れ替えるswap()関数を作成し,プログラムを完成させよ.


void main()
{
    float a=2.0, b=3.0;
	
	printf("a=%f, b=%f¥n", a, b);
	
	swap(□a, □b);
	
	printf("a=%f, b=%f¥n", a, b);
}

(2)関数を用いて2つの整数の合計と平均を求めるプログラムを作成せよ.
*ただし,□に必ずしも記号が入るとは限らない.


void sum_ave(int □a,int □b,int □sum,int □ave)
{
	□sum = a + b;
	□ave = (a + b)/2;
}
 
void main()
{
	int a=10;	/* 整数1 */
	int b=4;	/* 整数2 */
	int sum=0;	/* 合計を入れる変数 */
	int ave=0;	/* 平均を入れる変数 */
	
	printf("a=%d, b=%d,sum=%d, ave=%d¥n", a, b,sum,ave);
	
	sum_ave(□a, □b, □sum, □ave);	/* 合計と平均を求める関数 */
	
	printf("a=%d, b=%d,sum=%d, ave=%d¥n", a, b,sum,ave);
}

(3)関数を用いて実数型配列の要素をキーボードから10個入力及び配列の中身表示プログラムを作成せよ.
*ただし,□に必ずしも記号が入るとは限らない.



void input(float □p, int □n)/*キーボードからの入力*/
{
	int i;
	
	for(i=0;i<n;i++){
		printf("data[%d]=",i);
		scanf("%f",□p+i);
	}
	
}
void output(float □p, int □n)/*配列の表示*/
{
	int i;
	
	for(i=0;i<n;i++){
		printf("data[%d]=%f¥n",i,□(p+i));
	}
}


void main()
{
	float data[10];
	float max,min;
	
	input(□data,□10);
	
	output(□data,□10);
}


(4)実数型配列の要素をキーボードから10個入力,平均値の算出,最大値と最小値の算出,
大きい順に並び替え(ソート),配列の中身表示の5つの関数を作成せよ.
ヒント:上の問題を用いるとわかりやすいプログラムが出来る.



void main()
{
	float data[10];	/* 配列を入れる */
	float max,min;
	
	input(data,10);					/* 10個の要素を入力する関数*/
	
	printf("平均値 = %f¥n",average(data,10));	/* 平均値を算出する関数 */
	
	max_min(data,10,&max,&min);			/* 最大値と最小値を算出する関数 */
 
	printf("最大値 = %f,最小値 = %f¥n",max,min);
	
	sort(data,10);					/* 大きい順に並び替える関数 */	
	
	output(data,10);					/* 配列の中身を出力する関数 */
}

授業終了時までのプログラムと完成した提出用プログラムをoh-meijiシステムを使って提出すること.
授業終了時に送るのは出席の確認用であり,完成した課題は提出用の回に送ること.
(提出期限を厳守し,提出用の回に提出しないと採点を行わない)