2 関数(11章)

2.1 関数とは何か

2.1.1 関数の役割

1年生の時に学習したFORTRANを覚えている人は、サブルーチンを思い出して欲しい。それ が、C言語の関数に相当する。いかなるプログラミング言語でも、このサブルーチンに相 当する機能がある。それだけ便利な機能であるということである。

C言語のプログラムは、プリプロセッサー(15章)と宣言文と、実行文からなる。これらの うちプログラム実行中にCPUが動作するのは実行文の処理だけである。後に学習するが、 プリプロセッサーはコンパイル時(gccコマンド使用時)の動作に使われる。宣言文で ある変数宣言などは、プログラムがメモリーにロードされる際にそれを予約するのに使わ れるだけである。メモリーにロードされたプログラムが実際に動作するのであるが、その 動作内容は実行文に従う。コンピューターは、プログラム中の実行文の順序に従い、自動 的にメモリーの内容を処理しているだけである。

この実行文は、必ず関数のブロック中に書かれなくてはならない。今まで、学習してきた プログラムでは、main関数のみがあり、その中に実行文が書かれていたはずである。 main関数は特別で、C言語のプログラムには、必ず1個必要で、そこから実行される ことになっている。

実際のC言語のプログラムは、main関数のみからできていることは希で、リスト 1のように複数の関数からできている。

リスト 1: 処理を関数でまとめたプログラム

01: #include <stdio.h>
02: 
03: double max(double a, double b);
04: 
05: /*=====================================================================*/
06: /*    メイン関数                                                      */
07: /*=====================================================================*/
08: int main(){
09:   double x, y, z;
10: 
11:   x=2.5;
12:   y=3.1415;
13: 
14:   z=max(x, y);
15: 
16:   printf("大きい方は、%eです。\n", z);
17: 
18:   return 0;
19: }
20: 
21: /*=====================================================================*/
22: /*    大きい方を探す関数                                               */
23: /*=====================================================================*/
24: double max(double a, double b){
25:   double big;
26: 
27:   if(a<b){
28:     big = b;
29:   }else{
30:     big = a;
31:   }
32:  
33:   return big;
34: 
35: }
関数は、長いプログラムを効率よく記述するために必要である。そのために、この関数には2つの役 割がある。一つは、同じような処理を一つにまとめることである。実際のプログラムの動 作は、同じ処理、あるいは似たような処理が非常に多い。いちいちそれを書くとプログラ ムが長くなり、プログラマーは大変である。そこで、一つにまとめ、必要なときに呼び出 すのである。

もう一つ、長いプログラムの問題は、処理が分かりにくい点である。例えば、 windows2000だとソースプログラムは大体4000万行だと言われている。この場合、それぞ れの実行文の役割など分からない。コンピューターは大量のトランジスターからできてい るが、それぞれの役割が分からないのと同じである。このように大量の部品(実行文)から 構成されるコンピューター(プログラム)の動作を考える際に重要なことは、モジュール 2に 分解することである。そうすると、動作の内容が分かるようになる。長いプログラムを作 る場合も同じで、機能単位(モジュール)に分け、分かりやすくすることが重要である。C 言語では関数を使い、機能単位にプログラムを分割する。

まとめると、関数の役割は

である。特に、2番目が重要で、「プログラムのソースは分かりやすくなくてはならない」 ということを、肝に銘じておかなくてはならない。

[練習1]
リスト1をプリプロセッサーの文と宣 言文、実行文に分けよ。
[練習2]
リスト1のプログラムの実行順序を考 えよ。

2.1.2 同じ処理をまとめる

関数の役割の一つの「同じ処理をまとめる」ことの例を示す。そのために、大きさ10の配 列a[]と大きさ100の配列b[]に整数の乱数を格納し、その最値を求めるプログ ラムを考える。リスト2のようなプログラムである。 find_max_no_func.c

リスト 2: 関数を使わない最大 値検索プログラム

01: #include <stdio.h>
02: #include <stdlib.h>
03: #include <time.h>
04: 
05: int main(void){
06:   int a[10], b[100], max_a, max_b, i;
07: 
08:   srand((unsigned) time(NULL));          /* 起動毎に異なる乱数発生のため */
09: 
10:   for(i=0; i<10;   i++) a[i]=rand();     /* 配列 a[] の値設定 */
11:   for(i=0; i<100;  i++) b[i]=rand();     /* 配列 b[] の値設定 */
12: 
13:   /* ---- 配列 a[] の最大値検索 ----- */
14:   max_a=a[0];
15:   for(i=1; i<10; i++){
16:     if(max_a<a[i]) max_a=a[i];
17:   }
18: 
19:   /* ---- 配列 b[] の最大値検索 ----- */
20:   max_b=b[0];
21:   for(i=1; i<100; i++){
22:     if(max_b<b[i]) max_b=b[i];
23:   }
24: 
25:   printf("max a=%d\n",max_a);             /* 最大値印刷 */
26:   printf("max b=%d\n",max_b);
27: 
28:   return 0;
29: }


no counter
リスト2をよく見ると、最大値を求める部分はほとんど同じ である。そこで、それを一つの関数にまとめることを考える。リスト 3のようにすれば良い。これで、プログラムがすっきりした。こうす ると、最大値を求めるアルゴリズムを変えたい場合でもプログラムの変更が容易である。 find_max_func.c

リスト 3: 関数を使用し た最大値検索プログラム

01: #include <stdio.h>
02: #include <stdlib.h>
03: #include <time.h>
04: 
05: int find_max(int n, int ix[]);            /* プロトタイプ宣言 */
06: 
07: /*=========================================================================*/
08: /*    main 関数                                                            */
09: /*=========================================================================*/
10: int main(void){
11:   int a[10], b[100], max_a, max_b, i;
12: 
13:   srand((unsigned) time(NULL));          /* 起動毎に異なる乱数発生のため */
14: 
15:   for(i=0; i<10;   i++) a[i]=rand();     /* 配列 a[] の値設定 */
16:   for(i=0; i<100;  i++) b[i]=rand();     /* 配列 b[] の値設定 */
17: 
18:   max_a=find_max(10, a);
19:   max_b=find_max(100,b);
20: 
21:   printf("max a=%d\n",max_a);             /* 最大値印刷 */
22:   printf("max b=%d\n",max_b);
23: 
24:   return 0;
25: }
26: 
27: /*=========================================================================*/
28: /*    最大値探索の関数                                                       */
29: /*=========================================================================*/
30: int find_max(int n, int ix[]){
31:   int i, max;
32: 
33:   /* ---- 配列 a[] の最大値検索 ----- */
34:   max=ix[0];
35:   for(i=1; i<n; i++){
36:     if(max<ix[i]) max=ix[i];
37:   }
38:   
39:   return max;
40: }


no counter

2.1.3 処理をまとめてプログラムを分かりやすく

関数のもう一つの機能「処理をまとめてプログラムを分かりやすく」の例である。リスト 2のメイン関数の部分の処理を分かり易くするために、関数 を用いた例をリスト4に示す。 find_max_full_func.c

リスト 4: 関数使用し た最大値検索プログラム

01: #include <stdio.h>
02: #include <stdlib.h>
03: #include <time.h>
04: 
05: /* ----  プロトタイプ宣言 -----  */
06: void make_data(int nx, int ix[], int ny, int iy[]);
07: int find_max(int n, int ix[]);            
08: void print_results(int a, int b);
09: 
10: /*=========================================================================*/
11: /*    main 関数                                                            */
12: /*=========================================================================*/
13: int main(void){
14:   int a[10], b[100], max_a, max_b;
15: 
16: 
17:   make_data(10, a, 100, b);                /* 乱数データ作成 */
18: 
19:   max_a=find_max(10, a);                   /* 最大値検索 */
20:   max_b=find_max(100,b);
21: 
22:   print_results(max_a, max_b);             /* 最大値印刷 */
23: 
24:   return 0;
25: }
26: 
27: 
28: /*=========================================================================*/
29: /*    データ作成                                                            */
30: /*=========================================================================*/
31: void make_data(int nx, int ix[], int ny, int iy[]){
32:   int i;
33: 
34:   srand((unsigned) time(NULL));          /* 起動毎に異なる乱数発生のため */
35: 
36:   for(i=0; i<nx;   i++) ix[i]=rand();
37:   for(i=0; i<ny;  i++) iy[i]=rand(); 
38:   
39: }
40: 
41: 
42: /*=========================================================================*/
43: /*    最大値探索の関数                                                       */
44: /*=========================================================================*/
45: int find_max(int n, int ix[]){
46:   int i, max;
47: 
48:   /* ---- 配列 a[] の最大値検索 ----- */
49:   max=ix[0];
50:   for(i=1; i<n; i++){
51:     if(max<ix[i]) max=ix[i];
52:   }
53:   
54:   return max;
55: }
56: 
57: 
58: /*=========================================================================*/
59: /*    結果の印刷                                                             */
60: /*=========================================================================*/
61: void print_results(int a, int b){
62: 
63:   printf("max a=%d\n",a);
64:   printf("max b=%d\n",b);
65:   
66: }


no counter

2.2 関数の作り方と使い方

関数を使うためには、以下の3つのことが必要である。図1を見 ながら、以下を理解せよ。
図 1: 関数を使ったプログラムの基本的な書き方
\includegraphics[keepaspectratio, scale=1.0]{figure/how_to_make_function.eps}

2.3 データを渡す方法(p.214)

関数を使って処理を行う場合、呼び出す側の関数とと呼び出される側の関数とでデータの 受け渡しが必要である。呼び出す側は値(あるいは変数)を用意し、呼び出される側は変数 を用意する。呼び出す側の値を呼び出される側の変数にコピーすることで、データの受け 渡しが行われる。呼び出す側の関数の値(変数)を実引数、呼び出される側の関数の変数を 仮引数と言う。図1に示しているので、実引数と仮引数の 違いを理解せよ。

関数へのデータの渡し方に、2つの方法がある(通常は値渡し)。

値渡し
呼び出す側と呼ばれる側の関数が各々変数を用意する。仮引数側の 変数の値は、実引数の側の変数の値のコピーとなる。呼ばれた関 数が変数の値を変えても、呼び 出した側の変数値には、影響が ない。
アドレス渡し
呼び出す側の実引数は、アドレスです。呼ばれる側は、ポ イ ンターを用意して、実引数のアドレスを受け取ります。呼ばれた 関 数が処理をすると、呼び出した側の実引数の変数にも影響が あります。

それでは、データの受け渡しについて、教科書を見ながら、練習せよ。

2.3.1 値による呼び出し(p.214)

以下の練習問題を実施せよ。
[練習1]
main関数から、角度[度]の値を関数に渡して、プログラマー 作成の関数で、$ \sin$$ \cos$$ \tan$を計算して、印刷す る。このプログラムを作成せよ。ただし、main関数で繰 り返し文を使い、0〜360[度]まで、1度間隔でデータを送るこ と。ヒントは以下の通り。
[練習2]
リスト5の変数i,jの値が入れ替わら ない理由を考えよ。
not_swap.c

リスト 5: 値渡しを用い たため、値が交換されない例

01: #include <stdio.h>
02: 
03: void swap(int i, int j);
04: 
05: int main(){
06:   int a=2, b=3;
07: 
08:   swap(a, b);
09: 
10:   printf("a=%d b=%d\n", a, b);
11: 
12:   return 0;
13: }
14: 
15: 
16: void swap(int i, int j){
17:   int temp;
18: 
19:   temp = i;
20:   i=j;
21:   j=temp;
22: }


no counter

2.3.2 アドレスによる呼び出し(p.216)

以下の練習問題を実施せよ。
[練習1]
リスト6の変数i,jの値が入れ替わる理由 を考えよ。
swap.c

リスト 6: アドレス渡しを用いたため、値が交換される例

01: #include <stdio.h>
02: 
03: void swap(int *i, int *j);
04: 
05: int main(){
06:   int a=2, b=3;
07: 
08:   swap(&a, &b);
09: 
10:   printf("a=%d b=%d\n", a, b);
11: 
12:   return 0;
13: }
14: 
15: 
16: void swap(int *i, int *j){
17:   int temp;
18: 
19:   temp = *i;
20:   *i=*j;
21:   *j=temp;
22: }


no counter

2.3.3 一次元配列を渡す(p.218)

以下の練習問題を実施せよ。
[練習1]
要素数が10000個の一次元配列に、整数の乱数を格納して、そ の最大値を求めるプログラムを作成せよ。

2.3.4 多次元配列を渡す(p.220)

以下の練習問題を実施せよ。
[練習1]
要素数が1000$ \times$1000の二次元配列に、整数の乱数を格 納して、その最大値を求めるプログラムを作成せよ。

2.4 戻り値を返す方法(p.222)

2.4.1 1個の戻り値を返す(p.222)

以下の練習問題を実施せよ。
[練習1]
教科書の例を参考にして、

$\displaystyle f(x)=x-\frac{x^3}{6}+\frac{x^5}{120}-\frac{x^7}{5040}$    

を計算する関数を作成せよ。そして、メインルーチンで、 $ x=0$$ x=3.14$まで、180等分した値を求めよ。この値は何 か?。注意事項は以下の通り。

2.4.2 複数の戻り値(p.223)

以下の練習問題を実施せよ。
[練習1]
$ \sin{2\theta}$ $ 2\sin\theta\cos\theta$の値を0〜360で1 [度]間隔で、を計算するプログラムを作成せよ。

2.5 配列の受け渡し

配列の受け渡しは、変数の場合と異なる。1変数の場合、実引数のコピーが仮引数に渡さ れることは、以前に述べた通りである(値渡し)。しかし、驚いたことに、配列の場合、実 引数のアドレスが、仮引数に渡される。FORTRANと同じである。なぜ、このような不統一が 生じるかというと、 ということが理由のようである。仕様でそのようになっているので仕方ないが、不統一の 感はぬぐない。この辺が、C言語は難しいと思う人が出てくる原因になっている可能性が ある。

この辺のことをリスト7でみる。関数transposeで行列を転置 3している。main関数のtranspose(a)で、 関数transposeに配列a[5][5]の先頭アドレスを渡している。配列のところで学 習したように、配列名は、その配列の先頭アドレスを表す。

先頭アドレスは、transpose関数では、

	transpose(int x[][5])
 
の形で受け取る。アドレスが渡されたので、ポインターで受け取ることも出来るが、そう すると、後々の操作が大変になるので通常は避ける。配列を受け取る場合は、配列として 受け取る方が断然良い。

ここで、受け取る配列の大きさが、x[][5]とすべて記述されていない。配列のところで、 学習したように、サイズは、指定の配列をアクセスするためのアドレスのオフセット計算 に用いる。したがって、オフセット計算に不用な一番左のサイズは書かなくても良い。も ちろん書いても良く、twx[5][5]でも問題はないが、間違える確率が増えるので通常は書か ない。 。

一方、配列定義のときは、コンパイラーはメモリーを確保するために、配列全体サイズが 必要である。したがって、x[5][5]のようにすべて記述しなくてはならない。このような 理由から、もっと多次元の配列、例えば、x[100][100][5][5]のような配列をhogehoge関 数側で受け取る場合、hogehoge(int x[][100][5][5])と書く。左端のみ省略可である。

not_swap.c

リスト 5: 値渡しを用い たため、値が交換されない例

01: #include <stdio.h>
02: 
03: void swap(int i, int j);
04: 
05: int main(){
06:   int a=2, b=3;
07: 
08:   swap(a, b);
09: 
10:   printf("a=%d b=%d\n", a, b);
11: 
12:   return 0;
13: }
14: 
15: 
16: void swap(int i, int j){
17:   int temp;
18: 
19:   temp = i;
20:   i=j;
21:   j=temp;
22: }


no counter
\fbox{結果}
11 12 13 14 15
21 22 23 24 25
31 32 33 34 35
41 42 43 44 45
51 52 53 54 55

11 21 31 41 51
12 22 32 42 52
13 23 33 43 53
14 24 34 44 54
15 25 35 45 55

2.6 グローバル変数によるデータの受け渡し(p.228)

グローバル変数を使うと関数の独立性が失われ、再利用のする場合、支障をきたす。独立 性の高い関数はコピーすると、そのまま他のプログラムでも使える。グローバル変数を使 うと、関数のみならず、グローバル変数もコピーする必要が生じる。さらに、グローバル 変数は全ての関数で使えるため、名前の衝突を考えなくてはならない。大きなプログラム になると、これは大変な問題を生じる。非常に分かりにくいバグの原因となるので、気を 付けなくてはならい。

この講義で諸君が作る程度の短いプログラムならば、グローバル変数を使っても良いだろ う。むしろポインターが分からなくて悩むよりは、グローバル変数を使った方がプログラ ムを楽しめて良いだろう。

[練習1]
リスト6のプログラムをポインターを使わない でグローバル変数を使ったプログラムに書き換えよ。

2.7 main関数への引数渡し(p.228)

教科書に書かれているこのテクニックは、よく使われる。しかし、本講義ではあまり使わ ないので説明しない。興味のある者は、自分で調べよ。
ホームページ: Yamamoto's laboratory
著者: 山本昌志
Yamamoto Masashi
平成17年6月6日


no counter