3 関数

3.1 関数(サブルーチン)とは何か

3.1.1 数学の関数との比較

関数、正確にいうとプログラマーが作成する自作の関数を学ぶ前は、main 関数1個だけ からなるプログラムを作ってきた。単純なプログラムの場合は、それでも良いが、少し複 雑になるとmain関数以外の自作の関数を使わなくてはならなくなる。実用的なC言語のプ ログラムは複数の関数から構成されている。

関数というものは、まず数学で学習する。入り口と出口に注目すれば、関数というのは一 定の個数の値を受け取って,それから決まる値を返す箱2のようなものである。その箱は、 特定の機能を果たす。数学では、

$\displaystyle y=f(x_1, x_2, x_3, \cdots, x_n)$ (1)

と書く。 $ x_1, x_2, x_3, \cdots, x_n$は独立変数と呼ばれるものである。独立変数 の値が決まると、関数$ f$の定義によって、従属変数がひとつ決まるのである。$ f$という 箱に、 $ x_1, x_2, x_3, \cdots, x_n$を入れると、ただひとつの値を返すのである。 この辺の話は、中学校の数学の時間に関数の定義として、しつこく学習したはずである。 これを知らない者は、この学校を卒業できないであろう。理解していないものは、心を入 れ替えて勉強しろ。

C言語の関数は数学の関数と似ている。先ほどの数学の例に倣うと、C言語では

        y=f(x1, x2, x3, ... , xn)
と記述する。数学では $ x_1, \cdots x_n$を変数と呼ぶのに対して、C言語を含む手続き 型プログラム言語では x1, $ \cdots$, xn を引数(ひきすう)と呼ぶ。また関数が返す値 を戻値(もどりち)と呼ぶ。これは、関数fが引数x1, x2, x3, ... , xnの値を 受け取って、定義された処理を実施し、戻り値を計算する。その戻り値は、代入演算子 (=)により、変数yにコピーされると解釈する。

3.1.2 関数の役割

C言語のような手続き型プログラミング言語における「関数」の役割は、一連の処理を一 まとめにして名前(関数名)で呼び出せるようにすることにある。一般に,プログラムは 個々の指令を密接に関連するものごとにまとめて記述することでわかりやすくなる。関数 はそのような命令群をプログラム本体から分離して名前をつけたもので,いわばプログラ ムの部品なようなものである。様々な部品が集まって機械が構成されるように、多くの関 数が集まってC言語のプログラムは出来上がるのである。

全ての関数はどれも優劣が無く、同じレベルにある。それぞれの関数は、互いに呼び出す ことにより、それ固有の処理を行う。そして、実際の処理の内容を表す実行文は、どれか の関数に含まれる。従って、処理の内容は関数の中に書かなくてはならない。

ただし、同じレベルにあるが、main関数だけは、少しだけ優位な立場にいる。全て の関数に全く優劣が無ければ、どこからプログラムを実行する順序を決めることができな い。そのため、main関数だけ、少し優先順位が高く、最初に実行されることになっ ている。

まとめると、関数というのは呼ばれると何らかの仕事をするプログラムのことである。こ れは一つの大きなプログラムの部品と考えてよい。この関数を用いることにより、プログ ラムを機能毎に分割できる。そうすることにより、

というようなメリットがある。

3.2 関数(サブルーチン)の作り方と使い方

関数をつくり、使うためには、ソースプログラムを図1の ように記述する。必要な記述は、 である。プロトタイプ宣言により、関数の入出力の仕様を示す。そして、関数の定義によ りその処理の内容を示す。実際に関数を動作させるために、関数をコールする。
図 1: プログラマーが作成した関数を含んだソースプログラムの書き方
\includegraphics[keepaspectratio, scale=1.0]{figure/how_to_make_function.eps}

3.2.1 プロトタイプ宣言

関数の定義より前に、必ずプロトタイプ宣言を書く。実際には、コールよりも前に関数の 定義を書けば、このプロトタイプ宣言を省くことは可能であるが、それは良くないスタイ ルである。プロトタイプ宣言はコンパイラに関数の引数の型と個数、それから戻り値を知 らせる役割がある。そうすることにより、ソースプログラム中で関数の使い方の間違いを チェックするのである。これは、非常にありがたい機能である。

このプロトタイプ宣言の書き方は、簡単で、実際には関数の定義の先頭部分をコピーして、 セミコロンをつければ良い。

プロトタイプ宣言を書くことにより、ソースプログラムを読みやすくなります。現在では、複 数のプログラマーによりひとつのプログラムが作成されるため、読みやすいあるいは分か り易いプログラムを書くことは重要である。

プロトタイプ宣言をまとめると

となる。

3.2.2 関数の定義

関数は、その動作をプログラマーが定義しないと、コンピューターはどのように処理して よいか分からない。プログラマーによって、ソースプログラム内にその関数の動作を記述 する。動作といっても、引数を受け取り、それを処理して、その結果を呼び出し元へ返す と言う内容を記述するだけである。

関数の定義をまとめると、次のようになる。

3.2.3 関数のコール

実際に関数を使う場合、それを使いたい場所で、引数を伴って関数名を書けば良い。関数 を使う動作をコールと言う。関数をコールするのはmain関数のみならず、他の関数 からもコールできる。また、それ自身の関数からもコールできる(再帰呼び出し)。再帰呼 び出しについては、2年生で学習する。

よく使われる処理は、関数にまとめ、必要なときにコールすれば良い。このように、よく 使われる処理をまとめると、プログラムの内容が分かり易くなる。関数は、まことに便利 な機能である。

3.3 データの受け渡し方法

関数を使う場合、処理するためのデータの受け渡しがもっとも難しい。これさえ理解すれば、関数は 完璧である。

3.3.1 戻り値が無い場合

この場合、プロトタイプ宣言や関数定義の文では、void(空っぽの)と宣言する。
	#include <stdio.h>
	void hello(void);           /* プロトタイプ宣言 */

	/*================================================*/
	/*========      メイン関数            =============*/
	/*================================================*/	
	int main(void){
	  int i;

	  for(i=0; i<100; i++){
	    hello();         /*  関数呼び出し */
	  }

	  return 0;
	}

	/*================================================*/
	/*=======        関数                ==============*/
	/*================================================*/	
	void hello(void){
	
	  printf("Hello World \n");

	}

3.3.2 戻り値が一つの場合(値渡しを使う)

次に示す例は、2倍角の公式

$\displaystyle \sin 2\theta=2\sin\theta\cos\theta$ (2)

を確認するプログラムである。角度を0〜360度まで、1度ずつ変化させてそれぞれを、表 示させている。
	#include <stdio.h>
	#include <math.h>

	double function1(double x);           /* プロトタイプ宣言 */
	double function2(double x);           /* プロトタイプ宣言 */

	/*==============================================================*/
	/*   main function                                              */
	/*==============================================================*/
	int main(void){
	  int kakudo;
	  double pi, theta, y1, y2;

	  pi=3.141592;

	  for(kakudo=0; kakudo<=360; kakudo++){
	    theta = kakudo*pi/180.0;
	    y1 = function1(theta);         /*  関数呼び出し */
	    y2 = function2(theta);         /*  関数呼び出し */
	    printf("%d\t%lf\t%lf\n", kakudo, y1, y2);
	  }

	  return 0;
	}
	

	/* <<<<<<<<<< これ以降は、サブルーチン >>>>>>>>>>>>>>>>>>>>>>>>>>>>*/

	/*==============================================================*/
	/*   sin(2x)の計算                                              */
	/*==============================================================*/
	double function1(double x){
	  double y;

	  y=sin(2.0*x);

	  return y;
	}


	/*==============================================================*/
	/*   sin(x)cos(x)の計算                                          */
	/*==============================================================*/
	double function2(double x){
	  double y;
  
	  y=2.0*sin(x)*cos(x);

	  return y;
	}

3.3.3 戻り値が複数の場合(参照渡しを使う)

ここで示すようなデータの渡し方を参照渡し(call by reference)と 言う。次のような、変数に格納されているデータを交換するプログラムを例にして、参照渡しを考える。 このような、プログラムでは実引数が2個必要で、戻り値も2個必要になる。今まで、学習 してきた方法だと2個のデータを返すことができない。これは、main関数とプログ ラマーが定義した関数でデータを記憶するメモリーがきっちり分けられて、戻り値が一つ しか返せないためである。

メイン関数とプログラマー定義の関数で同じメモリーにアクセスできれば、問題は解決す る。いろいろな方法があるが、ここでは後に示すプログラムで、参照渡しという技法を使 う。これにより、メモリーの状態が図2のようになる。プロ グラマー定義の関数とメイン関数が同じメモリーを使っているのである。そのため、プロ グラマー定義の関数で、メイン関数の変数の内容を書き換えることができるのである。

参照渡しを実現するためには、呼び出し元の実引数に&をつけ、呼ばれる関数の 仮引数に*をつける。使い方については、プログラムを見て欲しい。

	#include <stdio.h>
	void swap(int *i, int *j);           /* プロトタイプ宣言 */

	/*==============================================================*/
	/*   main function                                              */
	/*==============================================================*/
	int main(void){
	  int a, b;
	  char temp;

	  printf("a = ");
	  scanf("%d%c",&a, &temp);

	  printf("b = ");
	  scanf("%d%c",&b, &temp);
  
	  swap(&a, &b);               /* 関数呼び出し */

	  printf("a=%d  b=%d\n", a, b);

	  return 0;
	}


	/*==============================================================*/
	/*   データの入れ替え                                             */
	/*==============================================================*/
	void swap(int *i, int *j){
	  int temp;

	  temp = *i;
	  *i = *j;
	  *j = temp;

	}
図 2: データの参照渡しのメモリー内容。本当はちょっと違うが、ポインターを使 わないでの説明なので勘弁して欲しい。
\includegraphics[keepaspectratio, scale=1.0]{figure/call_by_reference.eps}

3.3.4 配列を受け渡す場合

これも、本当はちょっと難しいが、参照渡しよりも簡単に思える。ポインターと併せて説 明するのが普通であるが、ここではそれを行わない。配列のデータを関数間でどのように すれば、受け渡せるか、分かって欲しい。本当に細かいことは、2年生以降とする。

10人の英語の成績を処理するプログラムを例に示す。これは、10人分のテストの点数を読み込んで、 平均点と各人の平均点からの差を計算するものである。具体的には、次のような動 作をするプログラムである。

このプログラムの核は、メイン関数とプログラマーが作成した関数 diff_ave()で、同じメモリー上の配列を共有することである。

これは、非常に簡単で、次のようにする。たとえば、呼び出し元でhoge[100]という 配列を関数kansu()に送りたいのならば、

	kansu(hoge);
として、コール (call) すれば良い。一方、呼び出された関数側では、それを配列名 fugaとして使いたいならば、
	戻り値の型 kansu(fuga[]){
	  処理内容
	}
とする。これは、例のプログラムをよく見て欲しい。このようになっているはずである。

2次元以上の配列の場合も同じようになる。ただし、呼び出された側では、配列のサイズ が必要となる。たとえば、呼び出し元で、

	int hoge[100], fuga[200][300], foo[400][500][600];
	
	kansu(hoge, fuga, foo);
とする。そうして、呼び出された関数側では、それを配列名a,b,cとして使いたいならば、
	戻り値の型 kansu(a[], b[][300], c[][500][600]){
	  処理内容
	}
とする。要するに呼び出された関数側では、配列のサイズの左端を書かなくても良いので ある。書いても良いが、一般的には書かない習慣となっている。書かない方が、間違える 確率が減るからだろう。

	#include <stdio.h>

	int diff_ave(int n, int data[]);           /* プロトタイプ宣言 */

	/*==============================================================*/
	/*   main function                                              */
	/*==============================================================*/
	int main(void){
	  int seiseki[10], average, i;
	  char temp;

	  for(i=0; i<10; i++){
	    printf("%d番の成績 = ", i);
	    scanf("%d%c",&seiseki[i], &temp);
	  }
  
	  average=diff_ave(10, seiseki);          /*  関数呼び出し */

	  printf("平均点 = %d\n", average);
	  printf("平均点との差\n");

	  for(i=0; i<10; i++){
	    printf("%d番 = %d\n", i, seiseki[i]);
	  }

	  return 0;
	}

	/*==============================================================*/
	/*   平気値とその差の計算                                          */
	/*==============================================================*/
	int diff_ave(int n, int data[]){
	  int i, sum, ave;

	  sum=0;                      /* 初期化 */

	  for(i=0; i<n; i++){         /* 点数の合計の計算 */
	    sum = sum + data[i];
	  }

	  ave = sum/n;                /* 平均値の計算 */

	  for(i=0; i<n; i++){         /* 平均値との差の計算 */
	    data[i] = data[i] - ave;
	  }

	  return ave;
	}
図 3: 配列を受け渡す場合のメモリーの内容。これも、正確ではなく、大体の のイメージである。
\includegraphics[keepaspectratio, scale=1.0]{figure/call_dimension.eps}

ホームページ: Yamamoto's laboratory
著者: 山本昌志
Yamamoto Masashi
平成17年2月20日


no counter