大きくて複雑なプログラムを一度に作るのは人間には難し過ぎる作業です。そこで、問題を分け、それぞれの部分プログラムを個々に作り充分テストした後で組み合わせ
て大きなプログラムにします。
ここで、部分プログラムが依然として複雑であれば更に分解して人間が一度に理解できる規模にしま
す。これは計算機をCPU、メモリー、ハードディスク、モニター、キーボード等、の部品に分けて作り後で組み立てるのと同じです。
この様なプログラムの作り方は構造化プログラミングの要点です。C言語は構造化プログラミングを容易にする仕組みとして関数を用意しました。 関数はどこからでも呼び出せて自由に組み合わせることができます。関数の中で自分を呼び出す再帰と呼ばれる処理も可能です。関数はプログラム部品であり、部品を組み立ては関数の中で他の関数を順番に呼び出すことで部品を組み合わせてます。
これまではmain関数だけでプログラムを書いてきましたが、この方法だけでは複雑な処理のプログラムは切れ目の無い長いプログラムになって、一 度に見渡すことの難しい物になってしまいます。そこで、ここでは、プログラムを関数に分けることを学んでもらいます。まとまった処理を関数として部品化 し、main関数から適切な順番でこれらの関数を呼び出す形のプログラムを作りましょう。
関数の定義は以下のような構文になります。
戻り値型名 関数名(引数型名 仮引数名, ...)/*関数頭部*/ {/*関数本体(function body)*/
仮引数から戻り値を求めるプログラム
return 戻り値; }関数から戻せる値の個数は、0または1個だけです。
値を戻さない場合は戻り値型名をvoidにします。戻り値がないruturn文はreturn;と書きます。
※戻り値がない場合に限ってreturn文は省略することも許されています。(注)戻り値は1個だけですが、後で紹介する、ポインタを使うことで結果的に複数の値を戻すこと が可能です。
例題4-1
ここでは例題として三角形の面積を求めるプログラムを示します。
#include<stdio.h> double menseki(double, double); /* プログラム中で使用する関数のプロトタイプ宣言 */ int main(void) { double teihen; // 底辺 double takasa; // 高さ double area; // 面積 printf("底辺の長さを入力してください"); scanf_s("%lf", &teihen); printf("高さを入力してください"); scanf_s("%lf", &takasa); area = menseki(teihen, takasa); printf("三角形の面積は%fです\n",area); return 0; } double menseki(double b, double h) /* 面積を求めるための関数 */ { double s; s = b * h / 2; return s; }
大域変数と局所変数
関数の外で宣言された変数を大域変数(またはグローバル変数)と 呼ぶのに対し関数内部で宣言された変数を局所変数(または ローカル変数)といいます。 この区別は変数の名前が見える範囲(スコープと言う)による区別で、 局所変数はその関数の内側でのみ参照できます。
静的変数と自動変数
この他に静的変数 (static)と自動変数(auto)の 区別が有ります。静的変数はプログラムの開始時点で用意されプログラム終了まで有効な変数であるのに対し、 自動変数は関数が呼ばれたときメモリを用意し、関数がreturnで戻るまで有効な変数です。 プログラム例を示します。
例題4-2
#include<stdio.h>
int max=10;/*大域変数(寿命はプログラムの初めから終わりまで)*/
/*整数の積を書き出しmax個ごとに改行を入れる関数*/ /*仮引数m,n 関数を呼ぶ側で作られ消去される*/
void printTable(int m,int n)
{
static int count=0;/*静的な局所変数(寿命はプログラムの初めから終わりまで)*/
int p;/*自動変数(寿命はprintTable関数が呼ばれて戻るまで)*/
if(max<=count){/*max個の前に改行を入れる*/
printf("\n");
count=0;
}
p=n*m;
printf("%2d ",p);
count++;
/*変数pはここで消えるがcountはプログラムが終わるまで有効*/
}
/*main関数*/
int main(void)
{
int i,j;/*自動変数(寿命はmain関数が呼ばれて戻るまで)*/
for(i=0;i<max;i++)
for(j=0;j<max;j++)
printTable(i,j);/*ここで仮引数m,nが作られ実引数i,jの値が代入される*/
/*呼び出しが終わると仮引数m,nは捨てられる*/
return 0; } /*
[実行結果]
0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9
0 2 4 6 8 10 12 14 16 18
0 3 6 9 12 15 18 21 24 27
0 4 8 12 16 20 24 28 32 36
0 5 10 15 20 25 30 35 40 45
0 6 12 18 24 30 36 42 48 54
0 7 14 21 28 35 42 49 56 63
0 8 16 24 32 40 48 56 64 72
0 9 18 27 36 45 54 63 72 81
*/
関数は変数や引数のメモリをどのようにして決めるのでしょうか?C言語の場合はスタックとよばれる仕組みを用いています。スタック(stack)とは積み重ねること、あるいは積み重ねたモノのことです。メモリへのア クセスはその番地を指定して行いますが、スタックはメモリ番地の大きな方から小さな方へ番地を移動しながらデータを積み上げていきます。
データ先頭の位置はスタックポインタと呼ばれる専用メモリ(多くのCPUがスタックレジスタを用意している)に格納されています。 関数の仕事が終わると不要になったメモリーは開放されスタックの山は崩されます。
スタックの使い方は人間が仕事をしながら書類を積み上げるのと似ています。
関数は引数でデ−タを受けとりますが、呼び出し側に書かれた引数を実引数、 関数側が受け取る引数を仮引数といい区別しています。関数の呼び出し側が、仮 引数用のメモリを用意し、実引数の値を代入し関数を呼び出します。呼ばれた関数の側は仮引数を使い計算を行って結果を戻り値で返します。呼び出し側では戻 り値を受取、不要になった仮引数のメモリを破棄します。 ここで注意してほしいのは以下のような点です。
1)実引数と仮引数は別物
仮引数の値を変更しても実引数は変化しません。
実引数は定数、式、変数と値を与えるものなら何でもかまいませんが、仮引数は変数です。2)仮引数は関数を呼ぶ側が用意と破棄を行う。
同じ関数が何度も呼ばれる場合でも、呼ばれるたびに仮引数は別の変数です。関数が自分自身を呼び出す場合でも仮引数は新しく作られます。
[目次]
再帰処理とは、関数内部で自分自身の関数を呼び出すことを言います。 例えば、function()という関数がその関数内部で再び関数function()を 呼び出すとき、これを再帰処理と言います。再帰処理は、全く同じ処理を 繰り返して行う場合などに使用されます。もし、再帰処理を使わないと すると同じ処理の関数を必要な分だけ作成しなければならなくなり、 その結果、プログラムは無駄に大きくなってしまいます。
例題4-3
ここでは例題として整数の階乗を求めるプログラムを示します。
#include<stdio.h> int fact(int n) { if(n==0) { return 1; } else { return n*fact(n-1); /* 自己関数の呼び出し */ } } int main(void) { printf("3!=%d\n",fact(3)); return 0; } 実行結果 3!=6
上の例題においてn=3の場合を考えると、再帰処理の過程は下図のようになります。
[目次]
方程式の解を求める方法には一般に二分法やニュ−トン法が使われます。 ここで取り上げた逐次代入法はx=f(x)の解を求める方法で、f(x)の 微分の絶対値が1を越えない場合のみ使える方法です。しかし、プログラムが簡単なので取り上げました。例題ではちょっと複雑な例f(x)=(1-x*x) /(1+sqrt(1+x*x))に適応してみました。
逐次代入法はまずxに適当な初期値を代入した後でxnew=f(x)で新しいxの値xnewを計算 します。このxnewを次のxの値として繰り返し代入してゆくとxが解に収束する方法です。もちろん関数f(x)によっては収束せずに発散してしまいます。(f(x)の微分値が−1〜1の範囲にあれば収束します)
/* 逐次代入法で方程式 x=f(x) の解を求める*/ #include<stdio.h> #include<math.h> /*方程式f(x)*/ double f(double x) { return (1-x*x)/(1+sqrt(1+x*x)); } /*大域変数の宣言*/ double eps=0.00000001;/*収束判定条件*/ int max=50;/*計算回数の上限*/ int main(void) { /*局所変数の宣言*/ double x=0.5;/*xの初期値は0.5から*/ double xnew; int i; /*逐次代入法の実行*/ for(i=0;i<max;i++){ /*収束しなくてもmax回計算すると止まる*/ printf("%2d回目 x=%9.7f\n",i,x); xnew=f(x); /*xnewとxの差の絶対値が小さく収束したら終了*/ if(fabs(xnew-x)<eps)break; x=xnew;/*xnewを次のxにする*/ } if(i>=max)printf("収束しませんでした!"); return 0; } /*実行結果 0回目 x=0.5000000 1回目 x=0.3541020 2回目 x=0.4243951 3回目 x=0.3929815 4回目 x=0.4076103 5回目 x=0.4009139 6回目 x=0.4040045 7回目 x=0.4025834 8回目 x=0.4032380 9回目 x=0.4029367 10回目 x=0.4030754 11回目 x=0.4030116 12回目 x=0.4030410 13回目 x=0.4030275 14回目 x=0.4030337 15回目 x=0.4030308 16回目 x=0.4030321 17回目 x=0.4030315 18回目 x=0.4030318 19回目 x=0.4030317 20回目 x=0.4030317 21回目 x=0.4030317 22回目 x=0.4030317 */
[目次]
このファイル(p04.c)を使ってプログラムを作成すること。
マクローリン展開を用いて sin (x) を、0°〜90°まで10°刻みで計算するプログラムを作成せよ。
ただし、m=6までで計算し、math.hのsin()関数の結果と比較すること。
ヒント:
出力結果は、以下のようになります。
- 階乗(!)は、例題4-3の fact() 関数を利用する。
- 冪(べき)乗(指数)は、math.hをインクルードして、pow(x,y)関数を利用する。
- マクローリン展開の式を分母と分子に分けた方が、プログラムが見やすくなります。
- sin(x)のxは、ラジアンである必要があります。
- \( \pi \)(パイ)には、M_PIを利用する。M_PIを使う場合は、#include
の前で#define _USE_MATH_DEFINESとすること。
左は、sin()関数の結果、右のt_sin()は、マクローリン展開で計算した結果sin( 0 °) = 0.0000000000000000000 t_sin( 0 °) = 0.0000000000000000000 sin(10 °) = 0.1736481776669303300 t_sin(10 °) = 0.1736481776669303300 sin(20 °) = 0.3420201433256687100 t_sin(20 °) = 0.3420201433256692100 sin(30 °) = 0.4999999999999999400 t_sin(30 °) = 0.5000000000000793800 sin(40 °) = 0.6427876096865392500 t_sin(40 °) = 0.6427876096898834700 sin(50 °) = 0.7660444431189780100 t_sin(50 °) = 0.7660444431798448800 sin(60 °) = 0.8660254037844386000 t_sin(60 °) = 0.8660254044361379600 sin(70 °) = 0.9396926207859083200 t_sin(70 °) = 0.9396926256245060600 sin(80 °) = 0.9848077530122080200 t_sin(80 °) = 0.9848077804932393600 sin(90 °) = 1.0000000000000000000 t_sin(90 °) = 1.0000001272001482000
※ fact()の再帰でスタックを利用しているため、mをあまり大きくできません。mを大きくしてしまうと、計算が終了しないので注意しましょう。
[目次]