プログラムを複数個の関数に分割すれば, 全体像を分り易く,また短く記述できる.
外見的な関数の書き方や使い方だけでなく, 内面的なコンパイルの仕組みについても理解しよう.
関数はプログラムの部品であり, C言語では,関数の組み合わせによって,プログラム全体を作り上げる.
関数の分類:
例:printf(),scanf()
例:main()
なお,メイン関数 main() は特別なユーザ関数であり, プログラムを実行すると,この関数が最初に自動的に呼び出される.
部品の切り分けと組み合せとをうまく設計しておけば, 大規模なプログラムの開発でも,効率的に進められるようになる.
記述方法:
// ユーザ関数の定義 戻り値の型 関数名(型1 仮引数1, 型2 仮引数2, ..., 型N 仮引数N) { ... return (戻り値); } // メイン関数の定義 int main(void) { ... ... 関数名(実引数1, 実引数2, ..., 実引数N) ... // 関数の呼出 ... return (0); }
具体例:(書き方1)
// 実数 a, b の最大値を返す関数...関数名(max)と仮引数(変数名 a,b)は自由に設定可 double max(double a, double b) { if (a > b) return (a); // a の方が大なら a を返し,呼出元へ戻る return (b); // b を返し,呼出元へ戻る // else return (b); と書きたいかもしれないが... // return (a) の後に次の文 return (b) に連接することは絶対にないので... // else は冗長・不要.Keep It Short and Simple!! } // メイン関数...プログラムはここからスタート int main(void) { double m; ... m = max(1.23, 4.56); // max(1.23, 4.56) を呼び出すと... // max() 側で a = 1.23,b = 4.56 となる. // max() が戻り値 4.56 を返して来る. // その結果,main() 側で m = 4.56 となる. ... }
ところで,変数では,使う前に型宣言が必要だった. 関数でも同様に,呼び出し前に プロトタイプ宣言(戻り値と仮引数の型宣言)が必要となる. このため基本的には,ユーザ関数の定義はメイン関数の前に記述すること.
または場合によっては, ユーザ関数(呼び出され側)の定義を メイン関数(呼び出し側)の後に書いても良い. ただし,次の具体例のように, プロトタイプ宣言をメインの前に記述しなけばならない.
具体例:(書き方2)
double max(double a, double b); // 関数の宣言...型と名前だけ記述,末尾にセミコロン int main(void) { ... m = max(...); // max() って何型?あー宣言されてたー double だねー ... } double max(double a, double b) // 関数の定義...処理内容も記述 { if (a > b) return (a); return (b); }
特例措置:
例:int main(void)
例:後述の void bar(int n)
数学ライブラリ関数 sqrt() の利用例 sqrt.c:
#include <math.h> // ヘッダファイル math.h の取込...sqrt() の宣言 // sqrt() のプロトタイプ宣言がヘッダファイル math.h 内に書かれている. // それをソースファイルに取り込んで利用する. // ソース内で宣言し直す必要はない. int main(void) { return ((int)sqrt(100.0)); // 平方根関数 sqrt() の呼出 // sqrt() の定義は他のファイル(ライブラリ)内に書かれている. // それをコンパイル時にリンク(連結)して利用する. }
$ cc sqrt.c -lm -o sqrt # ライブラリ libm の連結 # ライブラリ関数の定義は,誰かが過去にコンパイル済みなので... # 再コンパイルではなく,リンク(連結)だけで使える $ ./sqrt $ echo $? # main() の戻り値を表示 10 # √100 = 10
なお, 超基本的な関数(printf() や scanf() 等)の ライブラリ libc については, cc ... -lc ... と指定しなくても自動的にリンクされる.
コンパイルコマンド cc は, 内部的には,次の3手順を実行する:
コンパイラの実行(機械語への変換)の前に, ソースコードの #include 行が ヘッダファイルの内容に置き換えられる. sqrt.c の置換結果:(抜粋)
#include <math.h>// math.h の内容に置換↓ // math.h 内のプロトタイプ宣言 ... extern double sqrt(double x); extern double sin(double x); extern double cos(double x); ... // extern は「関数の定義は外部(external,他のファイル内)に書いてあるよ」という意味 int main(void) { return ((int)sqrt(100.0)); }
置換されたソースコード(C言語)を変換(翻訳)し, オブジェクトコード(機械語)を生成する.
生成されたオブジェクト(ユーザ関数 main() 等の定義)と 既存のライブラリオブジェクト(ライブラリ関数 sqrt() 等の定義)とを 連結(リンク)し,実行可能なプログラムを生成する.
なお,ヘッダファイルの内容は,ソースと同様にC言語で書かれている. 一方,ライブラリファイルの内容は,ソースコード(C言語)ではなく, コンパイル済みのオブジェクトコード(機械語)となっている. Linux の場合, ヘッダファイル *.h はディレクトリ /usr/include/ 等に, ライブラリファイル lib*.* はディレクトリ /lib/ 等に, それぞれ保管されている.
実験室の環境では, sqrt() のプロトタイプ宣言は, /usr/include/math.h から更にインクルードされている /usr/include/x86_64-linux-gnu/bits/mathcalls.h にありました.
プリプロセッサの置換結果は, コマンド「cpp sqrt.c | less」で確かめられます.
また,ライブラリ libc.* と libm.* は /usr/lib64/ とか /usr/lib/x86_64-linux-gnu/ にあるでしょう.
関数定義が必要となる簡単な例として, 3つの非負整数 a,b,c を棒グラフ表示するプログラムを考えよう.
まず,これまで通りの方法でソースコードを作成すると, 次のようになるだろう.
bar.c:残念な記述例
#include <stdio.h> int main(void) { int a = 10; int b = 20; int c = 15; int i; // a の棒グラフ for (i = 0; i < a; i++) { printf("*"); } printf("\n"); // b の棒グラフ for (i = 0; i < b; i++) { printf("*"); } printf("\n"); // c の棒グラフ for (i = 0; i < c; i++) { printf("*"); } printf("\n"); return (0); }
$ cc bar.c -o bar $ ./bar ********** ******************** ***************
このソースでは, ほとんど同じ処理を3回も書いており, コードが無駄に長くなってしまっている. このように冗長なコードを 書いてはいけません.
共通部分をユーザ関数として定義し,1箇所にまとめよう. bar.c:改善例
#include <stdio.h> /* 棒グラフを描く関数 引数 n:棒の長さ,非負整数 戻り値:なし */ void bar(int n) // 戻り値なしの場合,型を void とする { int i; for (i = 0; i < n; i++) { printf("*"); } printf("\n"); // return; // 戻り値なしの場合,return 自体を省略してもよい } int main(void) // 引数なしも void とする { int a = 10; int b = 20; int c = 15;int i;bar(a); // a の棒グラフ bar(b); // b の棒グラフ bar(c); // c の棒グラフ return (0); }
なお,「/* 〜 */」は複数行に渡るコメント, 一方,「//」は行末まで一行だけのコメント. ユーザ関数には使い方のヒントとなるようなコメントを必ず記述しておこう. 特に,引数と戻り値の意味については,記述は面倒だが,後で楽をするために重要.
関数化によって,実行結果は変わらないが, ソースコードは短く・読み易くなった. はじめからこのように関数分割して (プログラムを複数個のユーザ関数に分けて) 作るべきだったんだ.
さらに,変数 a,b,c を配列化し,関数 bar() の呼出を反復化すれば, さらにコンパクトにも記述できますね.
関数定義の他の例として, 非負整数の累乗 z = xy を求めるプログラムを作成しよう.
まず,ユーザ関数を定義しない(メイン関数だけを定義する)場合のソース power.c:
#include <stdio.h> int main(void) { int x, y, z; int i; // 入力部 printf("x, y > "); scanf("%d %d", &x, &y); // 計算部 z = 1; for (i = 0; i < y; i++) { z *= x; // x倍をy回 } // 出力部 printf("%d^%d = %d\n", x, y, z); return (0); }
なお,計算部にある z *= x は, z = z*x と同じことだ. (z*x の計算結果を z の新しい値としている.) この計算を y 回繰り返すと, z の値が x の y 乗になる.
$ cc power.c -o power $ ./power x, y > 2 8 2^8 = 256
さて,このソースには例(1)のような無駄な重複は見られないが, まあ,計算部を関数化してみよう:
#include <stdio.h> /* 累乗を計算する関数 x:整数 y:非負整数 戻り値:x^y(xのy乗) */ int power(int x, int y) { int i; int z = 1; for (i = 0; i < y; i++) { z *= x; } return (z); } int main(void) { int x, y, z;int i;// 入力部 printf("x, y > "); scanf("%d %d", &x, &y); // 計算部 z = power(x, y); // 出力部 printf("%d^%d = %d\n", x, y, z); return (0); }
この変更で,ソースコードが長くなってしまったように感じるかもしれない. しかし,メイン関数だけに注目すると,計算部がかなり短かくなり, プログラム全体の見通しが良くなったとも思うハズだ. メイン関数の部分だけをちょっと眺めるだけで, プログラムの全体像を把握できるようになった. はじめからこのように分割しておくべきだったんだ.
さらに短く,計算部と出力部とを一体化してみよう:
... int main(void) { int x, y, z; // 入力部 ... // 計算・出力部 printf("%d^%d = %d\n", x, y, power(x, y)); ... }
(本日の課題1です.)
入力された整数 x の正/負に応じて 符号値 +1,-1,または 0 を出力するプログラム sign.c を作成せよ. ただし,符号関数 int sign(int x) を定義・利用すること.
実行例:
$ ./sign
整数 > 123
+1 # 「+」は表示しなくても,まあOK
整数 > 0
0
整数 > -45
-1
...
[Ctrl]+[C]
$
(本日の課題2です.)
以前作成した反復による割り算のプログラム div.c を適切に関数分割せよ. つまり,割り算を計算する関数を作成せよ.
(本日の課題3です.)
数学ライブラリ関数(sqrt(),sin(),cos(),等) を利用して, 何らかの数学関数 y = f(x) のグラフを描くプログラム graph.c を作成せよ. ただし,グラフの1点を描く関数 void plot(int y) を定義・利用すること.
実行例:(sin カーブ)
$ ./graph * * * * * * * * *
質問 Q1〜Q5 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.