講義第10回:C言語の基礎 --- 関数


Web ページについては、提出状況をチェックした。提出者のリストは ホームページ作成者のリストにあるので、自分のものがあるかどうか確認すること。
先週はグラフィックスの機能を使ってみた。グラフィックスの機能は、 すべて、関数で実現されている。今週は、それらはどんな風に作ること ができ、何をするかということについて学ぶ。

「値を返さない関数」(手続き)

数学では「関数」といえば、指数関数とか三角関数のように、変数に対 応して値が決まるものだが、C言語の場合は必ずしもそうではない。以下 の例で説明しよう。
/* procedure_sample
 *
 * (C) J. Makino Version 1.0 Jan 11, 1997
 */
#include <stdio.h>
#include <math.h>

#define PI  3.14159265358979

void print_volume(double radius)
{
    printf("Radius = %15.7f ", radius);
    printf("Volume = %15.7f\n", radius*radius*radius*PI*4.0/3.0);
}

main()
{
    double x;
    printf("Enter radius : ");
    scanf("%lf",&x);
    print_volume(x);
}
このプログラムは、単に適当な数字を読み込んで、その値を半径とする球の体 積を表示するプログラムである。このプログラムでは、実際に体積を計算して 答を表示するのを、 print_volume という名前の関数が行なってい る。

この「関数」は英語の function の訳語であるが、数学的な「関数」と いうより、機能とか働きとかいった意味合いに近い。ただし、すぐあと で説明するように、値を返す関数というものもあり、こちらは数学的な 意味での関数に少し似ている。 値を返さない関数は、

[void] 名前(型 引数1 [,引数2, ...] [, 型[ ... 引数i [,引数i+1,...]])
{
    [変数宣言]
    実行部
}
という形をとる。このような記述が、プログラムのなかにあると、 もとのプログラム、つまり main() で始まっているところの実行部のな かからここで新しく作った関数を「呼び出す」ことができる。

なお、 C 言語では、 main() で始まるところ、つまり主プログラムも他 の関数と形式的には同じ形をしている。これは、言い換えると、「Cで書 いたプログラムでは、 mainという名前の関数から実行が始まる」という ことができる。

細かい話になるが、少し古い本や、人のプログラムを見ると、関数が、

void print_volume(double radius)
{
}
という形でなく、
void print_volume(radius)
double radius;
{
}
という形になっていることがある。これは、ATTのベル研で最初にできた C言語の決まりがそうなっていたからである。しかし、この形だといろい ろ不便なことがあるので、現在の形に変更された。古い形でもプログラ ムを書くことはできるが、しない方がいい(理由は後で述べる)

さて、上のプログラムを実行すると何が起こるかを考えてみる。メインプログラムで print_volume(x); というところまで来ると、次に実行されるのは void print_volume(real radius) の実行部の先頭の文 printf("Radius = %15.7f ", radius); である。関数の中の変数 radius の値は、メインプログラムで手続きを呼び出す時に設定した値、 すなわちメインプログラムの変数 x の値がそのままコピーされている。

手続き実行部の2行めの printf文で、球の体積が出る。これ が実行部の最後なので、その後メインプログラムに戻る。メインプログ ラムは戻ればもう終っているので、これですべてのプログラムがおしま いである。

実行例

Enter radius : 2
Radius =       2.0000000  Volume =      33.5103216

「なぜこんなことをするか」ということ

上のプログラムでは、関数を使ったからといって何かいいことがあるわけで はない。例えばプログラムの長さは、関数を使わないほうが短い。

しかし、ある程度複雑なプログラムで、同じようなことをあちこちでする時に は、その処理をまとめておいて、同じようなことを繰り返してプログラムしな くてもいいようにしたい。そのような時に関数は役に立つ。

もちろん、 for, while などを使った繰り返しでも、ある程度のことはできる が、さらに「関数」という形でまとめることで、便利に使えるようになる。

例えば、グラフィックスで使った initgraph, line, circle といった関 数は、中では実際には非常に複雑な処理をしている。これがあらかじめ 関数としてまとめられているおかげで、我々は簡単に画面に絵を描けるわけである。

(値を返す)関数

/* bisection
 *
 * (C) J. Makino Version 1.0 Jan 11, 1997
 */
#include <stdio.h>
#include <math.h>

double f(double x)
{
    double y ;
    y = x*x*x - 2;
    return y;
}


void bisection(double *  xmin,
	       double *  xmax,
	       double eps)
{
    
    double x, f_min, f_max;
    f_min = f(*xmin);
    f_max = f(*xmax);
    if (f_min * f_max  > 0.0){
	printf("cannot find solution...\n");
    }else{
	while(*xmax - *xmin > eps){
	    x = (*xmin + *xmax) *0.5;
	    if (f(x) * f_min > 0.0 ){
		*xmin = x;
	    } else{
		*xmax = x;
	    }
	    printf("x= %20.16f f(x)=  %e\n", x, f(x));
	}
    }
}

void main()
{
    double x0,x1, eps;
    x0 = 0.0;
    x1 = 2.0;
    eps = 1e-10;
   bisection( &x0, &x1, eps);
   printf("Final x = %20.14f %20.14f\n", x0,x1);
}
ここでは、「関数」らしく値を返すものを使ってみている。値を返さな いものとの違いは、

void 名前(引数の宣言);

の代わりに

型 名前(引数の宣言);

となることと、実行部の最後で、

return 式;
の形の戻すべき値を指定することである。このようにして宣言した関数 は、 C言語の標準のライブラリに入っている sin, cos など の関数と全く同じように使うことができる。

関数では、値を一つしか返せない。したがって、上の例のように、二分 法で方程式を解いて、区間の両端の値を戻したければ、引数の形で返すことに なる。

とはいうものの、最初の例のところで上に書いたように、Cでは関数の引 数の値はコピーされる。で、コピーされた方を書き換えても、元の値は 書き換わらない。元の変数の値を書き換えるためには、変数ではなく 「変数へのポインタ」を引数にする。

これはどういうことかというと、ポインタというのは、「変数がどこにあ るか」、つまり変数のアドレスを表現するのに使うものである。宣言は

double * pointer_to_double;
int    * pointer_to_int;
といったふうに、*をつけて宣言する。scanf のところ(C言語 の一回めの資料を見ること)でやったように、変数名の前に & をつ ければその変数のアドレスになる。逆に、ポインタの指している場所の 値を得るには、その前に*をつけることになる。

上のプログラムは何をしているか-方程式を解く

上のプログラムはそもそも何をするものかということをまだ説明していなかっ た。

上のプログラムは、「2分法」というやり方で、方程式の(近似的な)解を求 めるものである。方程式は、関数=0 という形になっているものとしよう。 このやりかたでは、まず最初にどの範囲に答があるかは知っているものとする。 そうすると、下図にあるように、その範囲の両端で関数の符号が違っているはず である。

その区間の中点で関数の値を計算する。図のように、中点での値と左端での値 の符号が同じなら、答えは中点と右端の間にある。この時は、中点の値で左端 の値を置き換える。逆に中点での値と左端での値の符号が違えば、もちろん答 えはその間にある。この時は右端の値を置き換える。いずれの場合でも、答が あるとわかっている区間の幅がもとの半分に狭まる。これを繰り返していって、 答をもとめる。

これは、例えば辞書で単語を探す時に、まず真ん中あたりを開いてみて、探し ている単語がそのページよりも後ろなら、後ろ半分のさらに真ん中あたりを開 く。というのを繰り返していくのと全く同じことである。人間がやるとまだるっ こしいが、計算機は速いので、こういうやり方でも結構あっというまにかなり 正確な答えにたどり着くことができる。

関数の宣言と「スコープルール」

以下に書くことは、必ずしも本質的ではないが実際にプログラムがどう 動くか、あるいは動やって書くかを理解するには結構重要なことである。

関数のプロトタイプ宣言

今日やった例では、
double f(x)
{
...
}
void bisection(...)
{
   ... = f(...);
}
main()
{
...
   bisection(...);
}
といった風に、「使う関数はあらかじめその前に定義されている」とい う形になっている。で、例えば引数の対応が間違っているとコンパイラ がチェックしてくれる。これでうまくいくのはまあいいような気がするわけ だが、良く考えてみるとちょっと変である。

というのは、scanf, printf, initgraph, line といった関数はプログラ ムの中で定義されているわけではないのに、ちゃんと使えているからで ある。このために使っているのが、「プロトタイプ宣言」と呼ばれる機 能である。プロトタイプ宣言は、例えば以下のような形をしている。

 void bisection(double * xmin,
	       double *  xmax,
	       double eps);
つまり、関数の最初のところだけ書いて、その後に実行部をつけないで セミコロンを書いておしまいにしたものである。これは、「この名前の 関数はこういう引数をもらって、こういう値を返します」ということを コンパイラに教える役割をはたしている。例えば line といった関数に は、このプロトタイプ宣言が xgrah.h というファイルの中に書いてある ので、これを #include で取り込んでいる。

この、プロトタイプ宣言というものは、要するにある関数について、 「それが外からどう見えるか」を規定している。えらそうにいえば「イ ンターフェース」を決めているということもできる。

スコープルール

スコープというのは何かというと、例えば変数であれば、宣言した変数 をどこで使うことができるかということである。宣言の有効範囲という こともできる。例えば

double f(x)
{
    double y;
    ...
}
void main()
{
    double y;
    .....
}
というプログラムを考えてみる。ここでは、 f(x) の中の変数 y と、 main の中の変数 y は全く別物であって、例えば f(x) の中で y を書き 換えたらその結果が main に伝わったりはしない。物理的には、これは、 メモリの違う場所におかれるということを意味している。

こういうことができるのは、ある関数の中で宣言した変数は、その関数 のなかでだけ有効だからである。その関数以外の関数が、勝手に変数を 書き換えたりはできない。これは、不便なような気がするかもしれない が、大きなプログラムを作るという場合とか、たくさんの人が協力して プログラムを作る時とかには非常に重要な機能である。

ただし、抜け道も準備されている。例えば

double y;
double f(x)
{
    ...
    y = ...
}
void main()
{
    f(x) = ...
    printf("y=%f\n",y);
}
といったように、「関数の外」で変数を宣言することもできる。この場 合には、変数宣言の下に出てくるどの関数でも、この変数を使うことが でき、それらはみな同じものである。したがって、上の例のように、fの 中でその変数に代入し、 main の中でその値を見れば、fで入れた値が出 てくることになる。

練習

以下にいくつか練習問題を出すので、時間に余裕がある人はやってみて欲しい。
  1. 前回にやったグラフィックスの手続きを使って、「二点の座標を与えると、 それを左上と右下の座標として長方形を書く」手続き
    void rect(int x1, y1, x2, y2);
    
    をつくる。これを使って、大きさ、位置を変えながらたくさんの長方形を画面 に書くプログラムを作ってみる。

  2. 画面の左下隅の方に、
    96 LII-1 620001 J. Makino
    
    というような形でサインを書く手続きを作る。なお、画面に字を書くには、手 続き outtextxyを使う。

  3. 2分法のプログラムで、関数の形をある程度変えられるようにしてみる。 具体的には、例えば3次関数 f(x)= ax^3 + bx^2 + cx + d (x^3は xの3乗のつ もり)で、係数 a, b, c, d の値を読み込めるようにしてみよう。

  4. さらに、区間の両端の値も読み込めるようにプログラムを変えてみよう。

  5. さらに、区間を指定したらその範囲のグラフも書くようなプログラムを つくってみよう。グラフを書くのは一つの関数にまとめておくこと。

次回予告

来週は、配列というものについて学ぶ。