第5回:関数とポインタ (10/22)


第4回演習:解答例


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

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

今回の演習の目標


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

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

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


#include <stdio.h>

int funca(void)
{
    int x;      /*この変数は main 内の x とは別物*/
    int y;

    /*何らかの処理*/

    return y;
}
変数 x, y の有効範囲

関数 funca の中のみ有効


void main()
{
    int x;      /*  この変数は funca 内の x とは別物  */
    x = 3;
    y = 4;     /* ×エラー ! yはmain関数内で定義されていない*/    
}
main 関数内では,変数 y は使えない

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


#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)実数型配列の要素の平均値を求める関数を作成せよ.
例えば,配列データをfloat data[] = {3.4, 2.0, 7.4, 5.0, 2.7, 9.6, 6.2, 4.9, 8.2, 2.2};とする.

(3)整数型配列の要素をキーボードから10個入力し,その最大値と最小値を求めるプログラムを作成せよ.

(4)10要素の実数型配列を作り,小さい順に並び替える(ソート)プログラムを作成せよ.
ソートの方法は,前期10回:並び替えを参考.
ヒント:(1)のswap関数を用いるとわかりやすいプログラムが出来る.

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


また提出の行い方については,以下のページを参照してください.
課題の提出方法