解読しやす易く無駄の無いソースコードを書くために,関数を活用しよう.
3C 原則(correct, clear, compact)は, 「高品質」な実務文章(レポート,論文,ニュース,等)を作成するための秘訣 として知られている.
この原則は作文だけでなく,プログラミングにも通じる:
解読し易く無駄の無いソースコードを書くには, 処理の意味的なカタマリや類型的なパターンを見つけ出して, 別の場所に括り出しておけばよい. この「ひとまとまり」の処理手続は, C言語では,関数と呼ばれている. これは,KTurtle での learn や, Scheme での define とか lambda, 等とほぼ同じものだ.
プログラムの関数化は,数学の因数分解のようなものだ. たとえば,式 a x + b x を (a + b) x に変形するように, 共通項(x)とその他(a,b)とを分類整理すればよい.
文章であれば,たとえば, 「今日の朝,A君は,寝坊してしまったので, 朝食をとらずに登校しました. 今日の朝,Bさんは,服を決めるのに時間をかけすぎてしまい, 朝食をとらずに登校しました.」 これだと,クドいし,小学生レベルの作文だよね? 3C を意識すると, 「今朝,寝坊したA君と服選びに手間取ったBさんは, どちらも食事抜きで登校した.」
最初の例として, 3つの整数 a, b, c を棒グラフ表示するプログラムを考えよう.
まず,これまで通りの方法でソースコードを作成すると, List 1 のようになるだろう. (データ入力は面倒くさいので,今回はとりあえず,データ値を決め打ちとしておく.)
main() { int a = 10; int b = 20; int c = 15; int i; // a の棒グラフ for (i = 0; i < a; i++) { printf("a"); } printf("\n"); // b の棒グラフ for (i = 0; i < b; i++) { printf("b"); } printf("\n"); // c の棒グラフ for (i = 0; i < c; i++) { printf("c"); } printf("\n"); }
ほとんど同じ処理を3回も書いており, プログラムが無駄に長くなってしまっている.
では, 共通部分を関数化してみよう. List 1 を元にして,List 2 のように改造しよう.
/* 棒グラフを描く関数 */ bar(int value, char symbol) { int i; for (i = 0; i < value; i++) { printf("%c", symbol); } printf("\n"); } /* メイン関数 */ main() { int a = 10; int b = 20; int c = 15; bar(a, 'a'); // a の棒グラフ bar(b, 'b'); // b の 〃 bar(c, 'c'); // c の 〃 }
関数化によって,実行結果は変わらないが, ソースコードは短く,読みやすくなった.
なお,反復と配列も使えば, 次のように,さらにコンパクト化することも可能:
... main() { int value[3] = {10, 20, 15}; char name[3] = {'a', 'b', 'c'}; int i; for (i = 0; i < 3; i++) { bar(value[i], name[i]); } }
bar( ) の呼び出しを1個にまとめられた.
データ入出力や数学関数など, 多くのプログラムで共通に利用されるような基本的な処理手続きは, ライブラリ関数として,既に用意されている. これまでに利用してきた printf( ) や scanf( ) は, ライブラリ関数の例だ.
一方,ソースファイルの中に新たに定義する関数は, ユーザ関数と呼ばれている. なお,これまでのCのソースではすべて, 先頭に main( ) が記述されていたが, これは特別なユーザ関数の定義だ. プログラムを起動すると,このメイン関数が最初に呼び出されることになっている. また,メイン関数の中から, 他の関数(ユーザ関数やライブラリ関数)が呼び出されることになる.
実用的なCのプログラミング作法では, 複数の小さな関数を部品のように組み合わせることによって, ひとつの大きなプログラムを構成することになる.
次の例として, 整数のべき乗 z = xy を求めるためのユーザ関数 power( ) を定義してみよう.
まず,List 3 は, 関数を定義しない場合(メイン関数だけを定義する場合)のソースコードだ.
main() { int x, y; int z, i; printf("x, y > "); // 入力部 scanf("%d %d", &x, &y); z = 1; // 計算部 for (i = 0; i < y; i++) { z *= x; } printf("%d^%d = %d\n", x, y, z); // 出力部 }
なお,計算部にある z *= x は, z = z*x と同じことだ. (z*x の計算結果を z の新しい値としている.) この計算を y 回繰り返すと, z の値が x の y 乗になる.
さて,List 3 には,List 1 のような無駄な重複は見られないのだが,気にせずに, List 3 の計算部(べき乗の計算の処理手続き)を関数化してみよう. List 3 の内容を元にして,List 4 のように書き換えよう. (教科書 pp.30-31 にも同様なプログラムが載っている.)
/* x の y 乗 を計算する関数 */ int power(int x, int y) { int z, i; z = 1; for (i = 0; i < y; i++) { z *= x; } return (z); } /* メイン関数 */ main() { int x, y, z; printf("x, y > "); // 入力部 scanf("%d %d", &x, &y); z = power(x, y); // 計算部 printf("%d^%d = %d\n", x, y, z); // 出力部 // 計算部と出力部を1つにまとめて... // printf("%d^%d = %d\n", x, y, power(x, y)); としても OK. }
List 3 と比べると List 4 では, ソースコードが長くなっているように感じるかもしれない. しかし,メイン関数だけに注目すると, べき乗の具体的な処理の記述がなくなっている分,かなり短かくなり, 「プログラム全体の見通しがよくなっている」と思うハズだ. (メイン関数の部分だけをちょっと眺めるだけで, プログラムの全体像を把握できるようになった.)
なお,記号 /* と */ で囲まれている部分は, コメント(説明文)である.(これまでにも使っていた.) これは,プログラムの動作とは何も関係ないので, 書かなくても実行できる. しかし,後々のメンテナンスのことを考えると, 関数についての最低限の説明については, 必ず書いておくべき.
記号 // から行の末尾までの部分もコメントだが, これは実は,C言語ではなくC++言語のコメント文. この授業では,短いコメントについては,// を利用する. こちらについては,授業用のコメント(学生向けの説明)なので, 必ずしも書かなくてよい.
また,他のプログラムの開発中に,べき乗関数が欲しくなった場合, List 4 の power() 関数のコードを変更なしにそのまま流用できる. これも関数化のメリットだ.
関数は,引数と戻り値を持つ. 引数(ひきすう)は関数の外部から関数の内部へ引き渡される値であり, 戻り値(リターン値,返り値)は関数の計算結果の値である. また,関数の処理内容の実行のことを関数呼び出しと言う.
List 4 の関数 power(x, y) の例では, x,y が引数, return (z) の z が戻り値である.
なお,呼び出し側と呼び出され側で引数を区別する場合には, 実引数(じつひきすう)と 仮引数(かりひきすう)という言葉が使われる. 関数の呼び出し時に,実引数の値が仮引数に代入される. したがって,仮引数は変数でなければならない. 実引数は定数であっても変数であっても構わない. また,当然,実引数と仮引数の型は同じでなければならない.
次のコードに規則をまとめておく:
double func(int x, double y) // 仮引数は必ず変数 { ... return (...); // (...) は戻り値(関数と同じ型に) } main() { int x = 1; double z; z = func(12, func(x, 3.14)); // 実引数は何でも(定数でも変数でも関数でも)よいが,仮引数と型を一致させる ... }
関数は通常,定数や変数と同じように, int や double のような型を持ち, そして関数の型は,戻り値の型によって決まる. 一方,main( ) のように戻り値や型をもたない関数も存在する. このような場合,return や型宣言を省略してよい. また,戻り値が無いことを表わすためのデータ型として, void 型も用意されている.
なお,場合によっては, 戻り値をもつ関数のことを 「関数」, 戻り値をもたない関数のことを 「サブルーチン」 と言って区別することもある.
関数化(サブルーチン化)には,次のようなメリットがある:
要するに,プログラムの開発効率を向上 (よいプログラムを素早く作成・改良)することが目的である.
しかし,関数化の方法の正解は1通りだけとは限らない.複数の方法があるハズだ. 読みやすくコンパクトなソースコードを作成するためには, 「処理手続きのどの部分をどのように関数化すれば効果的か?」を よく考えることが重要だ. 「闇雲な関数化は逆効果」になりかねない.
経験やセンスも必要. たくさんコードを書き,それらを何度も書き直し,経験を積もう. そして,他人の書いたコード(専門書やオープンソースソフトウェア)も参考にしよう.
ただし,鵜呑みにしてはダメだ. 世の中には,ひどいコードも多数公開されてしまっている. 本科目の課題レポートでは, 他人のコードの丸写しは大幅な減点対象となる. シラバス を確認せよ. 参考にした他人のコードについては,その良し悪しを考察するとともに, 独自の改善策を考え出すこと.
L01 double sqr(double x) L02 { L03 return (x*x); L04 } L05 L06 main() L07 { L08 printf("%f\n", sqr(2.0)); L09 } L10 // ← 左端の Lxx は行番号
余裕のある人は,等差級数 Σk ak, 等比級数 Σk ak 等などにも取り組んでみよう.