第5回. 関数 Written by Rorrim (待ってる人がいるのか怪しいですが...)、お待たせ致しました。 卒論やら、引越しやら、入学式やらでやたらと遅れましたが、関数のお勉強です。 今までの講義では、C言語を使う上で知っておかなくてはならない事、と言う イメージが強かったと思いますが、これから学ぶ「関数」を使うと 自分でプログラムを書く楽しみと言うものが増えてくると思います。 さて、C言語はMain関数が基礎となってその他の関数類を呼び出しています。 つまりMain関数がa_func,b_func,c_funcを呼び出してa_func,b_func,c_funcが それぞれe_func,f_func,g_funcを呼び出すと言う使い方も出来る訳です。 実際に書いてみましょう。 ---------------------prog_1------------------------------------------- #include void a_func(void); void b_func(void); void c_func(void); void e_func(void); void f_func(void); void g_func(void); main() { printf("Im in MAIN.¥n"); a_func(); b_func(); c_func(); printf("Finally, Back in MAIN.¥n"); } void a_func(void) { printf("Now Im in A_func.¥n"); e_func(); printf("Here, Back in A_func.¥n"); } void e_func(void) { printf("Now Im in E_func.¥n"); } void b_func(void) { printf("Now Im in B_func.¥n"); f_func(); printf("Here, Back in B_func.¥n"); } void f_func(void) { printf("Now Im in F_func.¥n"); } void c_func(void) { printf("Now Im in C_func.¥n"); g_func(); printf("Here, Back in C_func.¥n"); } void g_func(void) { printf("Now Im in G_func.¥n"); } --------------EOP------------------- %a.out Im in MAIN Now Im in A_func. Now Im in E_func. Here, Back in A_func. Now Im in B_func. Now Im in F_func. Here, Back in B_func. Now in C_func. Now in G_func. Here, Back in C_func. Finally, Back in MAIN. ---------------------------------------------------------------------- さて、どうでしょうか? プログラムの制御が関数の間を移動する様子に注意してください。 プログラムの実行は必ずmain()から始まるので、このプログラムの関数は 直接的であれ間接的であれ、このmain()によって呼び出されています。 言い換えれば、main()がすべての関数を起動するのです。 しかし、main()から関数を呼び出すからと言って、main()の中にプログラム 全体を詰め込まないでください。 なるべくプログラムを小さな機能ユニットの論理的集合体と考えて、 各ユニットを関数化するようにしましょう。 個々の関数が、論理的に独立したタスク(お仕事)を実行する、と言う事を 念頭に置いてプログラムを関数で部品化すればプログラミングやデバッグを 効率よく行う事ができます。 なぜなら、プログラムを書くと言う作業はソースを書く作業よりも デバッグに多くの時間を割くものです。 (ウチの研究室には壁に「プログラムは希望通りでなく、書いた通りにしか動かない」 と言う名言が掲げてあります) それでは関数の記述方法について考えましょう。 void a_func(void); | | | 1 2 3 1のvoidは、この関数がvoid型の値を「返す」と言う事を示します。 (「返す」と言う言葉はあとで説明します) 2のa_funcは関数の名前ですので分かりやすい名前を使ってください。 3の()で囲んだ部分が「引数の宣言」です。 (「引数の宣言」もあとで説明しますね〜) これが関数の記述方法です それではデータ型として今まで勉強した、int型の関数を使ったプログラム を例として「返す」と「引数」と言う言葉の概念をつかんでください。 -----------------------prog_2----------------------------------------- 01|#include 02| 03|int sum(int x, int y) 04|{ 05| int goukei; 06| 07| goukei = x + y; 08| return (goukei); 09|} 10| 11|main() 12|{ 13| int a, b, wa; 14| 15| printf("aの値を入力してください:"); 16| scanf("%d, &a); 17| printf("bの値を入力してください:"); 18| scanf("%d, &b); 19| 20| wa = sum(a, b); 21| printf("a + b = %d¥n", wa); 22|} --------------------EOP-------------------- %a.out aの値を入力してください:5 bの値を入力してください:10 a + b= 15 ---------------------------------------------------------------------- このプログラムは、ユーザが与えた任意の2つ値の合計を求めるプログラムです。 その為Scaf関数を使っています。 それでは流れにそって解説します。 (注:""内の等式は右から左への代入を意味します) 1.まずユーザはaの値とbの値を与えます。 2.その値はScanf関数によってそれぞれaとbに代入されます。 (つまりここでは"a=5,b=10"となっています) 3.次に"wa = sum(a, b)"によりSum関数が呼び出されますので、 Main関数は、Sum関数にa(つまり5)とb(10)の値を渡します。 4.Sum関数に実行が移り、Main関数によって渡されたa(5),b(10)の値が それぞれx,yに代入されます。 (ここでSum関数内のx,yは、x=a=5であり、y=b=10です) 5."goukei = x + y"が実行され、"goukei = 15"となりました。 6."return (goukei)"が実行され、Main関数に実行がうつります。 ここで"sum(a, b) = sum(x, y) = goukei = 15"となりました。 (と言うかそう思ってください、いい説明が浮かばないんです) 7.wa = sum(a, b)でしたので、wa = 15の代入が実行されます。 う〜ん、わかって貰えるか自信ないですがこれが説明になります。 呼び出す方のaとbの値を「実引数」(argument)といい、 呼び出される方のxとyを「仮引数」(parameter)といいます。 また、プログラムの流れはreturn文に出会うか、関数本体の終わりを示す"}"に 出会うとその関数を抜け出して呼び出し元に戻ります。 ちなみに4〜9行は { return (x + y); } と書いてもプログラム的に同じです。 さて、最後に「記憶の寿命」を説明しておわります。 ・記憶の寿命 関数の中で宣言された変数はプログラムの開始から終了まで存在している訳ではなく、 静的記憶寿命、自動記憶寿命の2つがあります。(記憶クラスとは別ものと考えてね) プログラムを書いて説明します。 --------------------------prog_3-------------------------------------- #include int fx = 0; /*自動記憶寿命をもつ外部変数*/ void func(void) { static int sx = 0; /*静的記憶寿命をもつ静的変数*/ int ax = 0; /*自動記憶寿命をもつ自動変数*/ printf("%3d%3d%3d¥n", fx++, ax++, sx++); } int main(void) { int i; puts(" fx ax sx"); for (i = 0; i < 10; i++) func(); return (0); } ---------------EOP--------------- %a.out fx ax sx 0 0 0 1 0 1 2 0 2 4 0 4 5 0 5 6 0 6 7 0 7 8 0 8 9 0 9 ---------------------------------------------------------------------- func関数は戻り値として何かを返す訳ではありません。 このような関数はvoid関数と宣言します。 今までは余り気にせずに使っていましたが、 それはC言語が値を返すものも返さないものも「関数」として扱う為です。 しかし他の言語では、値を返さないものを「サブルーチン」(FORTRAN)、 「手続き」(Pascal)などと違う概念で扱います。 自動記憶寿命 関数の中で「記憶クラス演算子」であるstaticを付けずに宣言・定義した変数は 全て自動記憶寿命を持つ事になります。 この変数には「プログラムの流れが、変数の宣言を通過する際に、初めてその変数 が作られる。その宣言を囲むブロックの終点である"}"を通過する時に、その変数は 役目を終えて消える」と言う性質があります。 静的記憶寿命 関数の中でstaticを付けたものや、関数の外で宣言・定義された変数は、 静的記憶寿命を持つ為「プログラムの開始時、具体的にはmain関数を実行する前の 準備段階において変数が作られ、プログラムの終了時まで生き続ける」と言う性質 をもちます。 この2つを念頭においてプログラムを見ていきましょう。 1.静的記憶寿命を持つfxとsxは、プログラムの開始時に作られ0に初期化されます。 2.main関数の実行が始まり、この時変数iが作られる。 3.main関数からfunc関数が呼び出される。この時axが作られ0に初期化される。 ここで"fx,ax,sx"の値はそれぞれ"0,0,0"と表示される。そのあと3つの値は インクリメントされて、それぞれ1となる。(1になるだけで表示はされない) 4.func関数の終了とともにaxは消える。 5.main関数はiをインクリメントして、再びfunc関数を呼び出します。 この時axがつくられ、0に初期化される。 3つの変数はそれぞれ"1,0,1"と表示される。各変数はインクリメントされて、 それぞれ"2,1,2"の値を持つ。 6.最終的にmain関数の終了と同時に変数iは役目を終えて消えます。 ・記憶クラス 全ての変数には「データサイズを規定するデータ型」があるだけでなく、 「メモリ上のどこに記憶されるかを規定する記憶クラス」と言うものがあります。 変数名はCコンパイラの側から見ると、その変数の値が記憶されているコンピュータ 内部の物理的場所を示しているにすぎません。データはメモリ上かCPUレジスタの いずれかに記憶されます。変数がメモリとレジスタのどちらに記憶されるのかを決定 するのが、その変数の記憶クラスです。 Cの記憶クラスには「自動変数」(auto)「レジスタ変数」(register) 「静的変数」(atatic)「外部変数」(external)の4種類があります。 自動変数 自動変数は以下の要領で宣言します。 ----------------------------prog_4------------------------------------ #include main() { auto int x = 1; { auto int x = 2; { auto int x = 3; printf("%d¥n",x); } printf("%d¥n",x); } printf("%d¥n",x); } ---------------EOP--------------- %a.out 3 2 1 ---------------------------------------------------------------------- 普通のCのプログラムでは、変数の記憶クラスを明示していない場合、自動変数と みなされますから、実際のプログラム上で"auto"を見る事はないと思います。 変数の寿命は自動記憶寿命となります。 レジスタ変数 レジスタ変数の宣言は以下のように自動変数と同じように宣言します。 また、自動変数と同じような使い方をし、同じような結果をもたらします。 その意味でこの変数は自動変数の変形であるといえます。 -------------------------prog_5--------------------------------------- #include main() { register int x = 1; { register int x = 2; { register int x = 3; printf("%d¥n",x); } printf("%d¥n",x); } printf("%d¥n",x); } ---------------EOP--------------- %a.out 3 2 1 ---------------------------------------------------------------------- 自動変数と何が違うのかと言うと、データの記憶場所です。 レジスタ変数は、メモリより高速に操作できるCPUレジスタにデータを収めます。 レジスタ変数が指定されるとコンパイラはフリーのレジスタを発見し、 そのレジスタが変数を収めるのに十分な大きさであればデータを収め、 十分でなければその変数をメモリに収めます。(つまり自動変数として処理します) registerを使用する場合は、どの変数をレジスタ変数に指定するかを 十分考慮してください。 使用可能なレジスタには限りがあり、コンパイラは宣言された順番に レジスタ変数を作成していきます。 静的変数 staticを用いて宣言し静的記憶寿命をもちます。 宣言の仕方等はprog_3を見てください。 静的変数は、それが本当に必要だと思われる場合のみに使用するようにしてください。 なぜなら静的変数は、それが実際に必要でないときも、常に値をメモリ中に保存して いるからです。 外部変数 外部変数は、関数の外で定義する変数で、自動変数のように特定の関数にとらわれず 広く存在し、その値を必要とする場合はどの関数からでも参照できます。 ----------------------prog_6------------------------------------------ #include int x = 123; main() { printf("%d¥n%d¥n", x, y); } int y = 456; ----------------------prog_7------------------------------------------ #include int x = 123; main() { extern int x, y; printf("%d¥n%d¥n", x, y); } int y = 456; ---------------------------------------------------------------------- たとえばprog_6は外部変数としてxとyを宣言していますが、外部変数の使用宣言 をしていないので「yが定義されていない」とコンパイラに怒られます。 しかしprog_7は外部変数x,yを使用すると"extern"を用いて宣言しているので コンパイルがとおります。 prog_3で使ったfxも外部変数ですので、記憶寿命についても説明する必要はない でしょう。 最後に記憶クラスを表にまとめておきます。 --------------------------------------------------------------------------- 記憶クラス 宣言例 説明 --------------------------------------------------------------------------- 自動変数 auto int a; 宣言されている実行単位ブロック全域で有効。 int a; 実行単位終了後は消滅。コンパイラは初期化 しない。 レジスタ変数 register int a; 宣言されている実行単位ブロック全域で有効。 実行単位終了後は消滅。コンパイラは初期化 しない。 静的変数 static int a; 宣言されている実行単位ブロック全域で有効。 実行単位終了後も存在。コンパイラが初期化 する。 外部変数 extern int a; 関数の外で宣言されていれば、広域的に有効。 但し、使用する関数の外で宣言する事。 使用する関数内で外部変数として宣言すれば どの関数からでも参照できる。実行単位終了 後も消滅しない。コンパイラが初期化する。 ---------------------------------------------------------------------------