5.実用的なプログラム
5.1 構造化と関数
プログラムをわかりやすく組み立てるための技術「構造化」と「構造化」を実現するための関数の使い方について学びます。 プログラムはコンピュータに目的の動作をさせるために作成します。もちろんプログラムは正しく働かなければならないことはいうまでもないことですが、単に働けばそれでいいかというと、それだけでは充分でない場合が多くあります。 例えば仕事として請負ったプログラムの場合、他人が見て分かり易く、読み易いプログラムでないといけません。またプログラムに変更を加える場合や、バグがあったような場合、修正や変更が容易に行えるよう配慮しておく必要もあります。そこで、この章ではプログラムの保守性や信頼性についてプログラムの書き方を考えます。 少し大きなプログラムになると全体の働きが複雑に込み入ってきて扱いにくくなります。しかし、ほとんどの処理内容はいくつかの部分に分割することが可能です。そして、それら全体を統括する機能部分が中心にあり、その統括のもとに部分的な処理が行われ、必要ならば、その部分的処理にはもう一段下の処理を設けるという具合に機能ごとに整理して考えます。 ┌─────┐ │ 主処理 │ レベル 1 └──┬──┘ ┌───────┼───────┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ │ 処理 1 │ │ 処理 2 │ │ 処理 3 │ レベル 2 └──┬──┘ └──┬──┘ └──┬──┘ ┌───┴───┐┌──┴──┐ └───┐ ┌──┴──┐ ┌──┴┴─┐ ┌─┴───┐ ┌──┴──┐ │ 処理 A │ │ 処理 B │ │ 処理 C │ │ 処理 D │レベル 3 └─────┘ └─────┘ └─────┘ └─────┘ <図A> 計算例 図Aにこのような構造を示してありますが、このような構造をモジュール構造といいます。また図の右に書いてあるレベルは処理の階層を意味し、上のレベルにある処理は下のレベルにある処理(モジュール)を利用することを表わしています。このレベル付けを階層化といいます。このモジュール化と階層化を行うことをプログラムの構造化といいます。 プログラムを作成するとき、解決すべき問題(目的)をまず解析し、その中に含まれている内容を上述のようなモジュール構造に分解し、階層的に構成します。この段階は手間がかかり、苦労しますが、構造的に物事を考えることは全体の働きを明確に把握でき、見落としやミスを除き、見通しも良くなります。 C言語でプログラムを作る場合、計画もなくいきなりコンピュータに向かってプログラムを打ち込み、行き当たりばったりで作成するという傾向を持つプログラマがいます。短いプログラムや使い捨てプログラムではこのような方法でもかまいませんが、サイズが大きくなるとヒモがもつれたような状態となり、どうにもこうにもならないことになりかねません。最近のパソコンなどでは、ほとんどがスクリーンエディタやウィンドゥを持っており、プログラムの書き込みや修正が簡単にできるようになっていますので、ついつい無計画なプログラミングをしてしまいがちです。 5.1.1 プログラムの作成手順 プログラムの作成では前段階で得られたモジュール構造の各部についてフローチャートを作成し手続きを明確にします。モジュール構造はモジュール間の関係を表し、フローチャートはモジュールを実行するための手続きの順序を表すという違いがあります。このようにして作成されたフローチャートに基づき各モジュールをプログラムします。プログラム化では使用する言語に応じてフローチャートの各処理を記述します。この時各モジュールの呼出はサブルーチンや関数として表します。C言語ではサブルーチンをすべて関数で表現します。 このようにしておくと、モジュール構造とプログラムの対応が付きプログラムが読み易くなります。C言語では、この構造化を行うことでプログラムをスッキリと表現することができます。図AをC言語の関数に当てはめて書き直すと、階層の一番にmain()がきて、一番下にprintf()などのライブラリー関数(すでにC言語の処理系の中で用意されている関数)がきます。 ┌─────────┐ │ 主処理:main() │ └──┬──────┘ ┌───────┼───────┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ │ 処理 1 │ │ 処理 2 │ │ 処理 3 │ ユーザ関数 └──┬──┘ └──┬──┘ └──┬──┘ ┌───┴───┐┌──┴──┐ └───┐ ┌──┴──┐ ┌──┴┴─┐ ┌─┴───┐ ┌──┴──┐ │ 処理 A │ │ 処理 B │ │ 処理 C │ │ 処理 D │ ユーザ関数 └──┬──┘ └──┬──┘ └─────┘ └─────┘ ┌──┴──┐ ┌──┴──┐ │ printf()│ │ sin() │ ライブラリー関数群 └─────┘ └─────┘ 図B C言語の関数の階層関係 5.1.2 関数の使い方 [基本事項]関数の宣言 C言語では関数を使うには前もってその関数の型を宣言する必要があります。プログラム全体で利用するにはリストの最初、関数内でのみ利用する場合は関数の先頭で宣言します。特例としてint型の引数をとるものや、関数の値がint型の場合は宣言が省略できます。 関数宣言の例 void sub(); ---- 関数の値をとらないvoid型のsubという名前の関数を宣言します。 double max(); ---- double型の値をとるmaxという名前の関数を宣言します。 int fac( int ); ---- int型の引数を1つとり関数の値もint型のfacという名前の関数を宣言します。 printf()やsin()などのライブラリ関数はstdio.hやmath.hなどのヘッダファイルの中で関数の型の宣言が行われます。ですからライブラリ関数の使用に先だってヘッダファイルのインクルードが必要なのです。またmain()も本来は宣言が必要ですがint型として扱うので宣言を省略できます。 [基本事項]関数の定義 型 関数名(型 変数名, ‥) { : return 式 ; } ・関数がvoid型の時はreturnは省略できます。 ●動作を呼び出す 関数を使った簡単な構造化の例を示します。リスト9.1を基本型とします。基本型の動作を10回くりかえす方法を考えます。ループ変数を設定した、リスト9.2が定番でしょう。しかし繰り返しを関数を使って実現したリスト9.3の手もあります。リスト9.3はこれだけでは、あたりまえすぎますが、関数の機能を2進数の形で増やしていくと無駄なく拡張できます。例えば20回繰り返す場合でもsub16()をつくれば対応できることがわかります。 <リスト5.1>基本型 /* KIHON.C */ #includeint main() { printf("abc\n"); } <リスト5.2>基本型を10回繰り返す /* kurika.c */ #include,stdio.h> int main() { int i; for(i=0;i<10;i++) printf("abc\n"); } <リスト5.3>基本型を10回繰り返す:関数を使う方法 /* sub00.c */ #include void sub2(),sub4(),sub8(); main() { sub8(); sub2(); } void sub2() { printf("abc\n"); printf("abc\n"); } void sub4() { sub2(); sub2(); } void sub8() { sub4(); sub4(); } ○星プログラムを関数化する
#includemain() { int i; for(i=0;i<5;i++){ sub(); } } sub() { int i; for(i=0;i<5;i++){ printf("*"); } printf("\n"); } 5.1.3 関数と引数
リスト5.3で定義した関数は関数の値を持ちません。このようにC言語では、値を返すモジュールも、他の言語では「手続き」と呼ばれるような値を戻さないモジュールも、全て関数として定義します。次にデータの受け渡しを行う関数について説明します。 一般にサブルーチンには、サブルーチンに渡すデータとサブルーチンからの実行結果のデータが介在します。C言語では、前者を関数の引数、後者を関数の値としてやりとりします。 ┌────────┐ │ サブルーチンへの入力──┤ サブルーチン ├──サブルーチンからの出力 │ a = sub(b,c,d); └────────┘ │ │ │ ┌────────┐ │ │ │ 関数への引数 ──┤ 関数 ├──関数の値 │ 関数の値 引数 └────────┘ │ ●引数を用いた関数によるサブルーチンの書き方 標準入力より入力した整数値の階乗を求めるプログラムを作ってみます。階乗を返す関数を独立させてみます。 <リスト5.4>では、整数値を引数として受取り、その階乗を整数で返す関数fac()が定義されています。このようにプログラマが定義する関数は他の関数、リスト9.4ではmain()、と並列に書いて使用します。このユーザ関数の定義はmain()の前に書いても後ろに書いてもかまいません。 関数を呼び出す場所より前方で定義された場合は、関数の宣言は省略できます。 <リスト5.4> /* func00n.c */ #includeint fac( int ); int main() { int i; scanf( "%d", &i ); printf( "%d!=%d\n" , i , fac(i) ); } int fac(int x) { int i,m=1; for(i=1;i<=x;i++) m = i * m; return(m); } ●関数を用いた星プログラム 星プログラムの1行描画を関数にしてみます。 リスト9.5 #include main() { int i; for(i = 1; i<=5 ; i++){ sub( i ); } } sub( int i ) { int j; for( j = 1; j <= i ; j++){ printf( "*" ); } printf("\n"); } ●関数と変数 C言語では関数と変数の間には以下のような性質があります。これらを確かめるプログラムをリスト9.6に示します。リスト9.6のmain()内の変数iとsub()内の変数iは独立した別の変数なのでsub()内で増分されたsub()内の変数iの値はsub()から戻ったあとのmain()内のiには反映されません。 [基本事項]関数内部で宣言した変数はその関数内部でのみ有効 [基本事項]引数は呼び出し側から呼ばれる側に一方向にのみ渡される。 リスト9.6 #include main() { int i; i = 1; sub( i ); printf("%d (main)\n", i); } sub( int i ) { i = i + 1; printf("%d (sub)\n", i); } 実行結果 2 (sub) 1 (main) ●関数と配列 関数の引数として配列を渡すことができます。その例をリスト9.7に示します。ところが実行結果をみるとリスト9.7ではmain()内で宣言した配列変数x[]の内容が変化しています。なぜでしょうか。リスト9.7の場合でも引数はmain()からsub()にのみ情報が渡されています。リスト9.6と違うところはリスト9.6では変数の値そのものが渡されますが、リスト9.7では変数の存在する場所が渡されます。ですから、リスト9.7のsub()ではmain()内の変数x[0]が直接操作されます。 C言語ではこのように変数の場所を情報として扱えます。この場所の情報を「ポインタ」と呼びます。「ポインタ」の中身はメモリ・アドレスの値です。 リスト9.7 #include main() { int x[10]; x[0] = 1; sub( x ); printf("%d (main)\n", x[0]); } sub( int x[] ) { x[0] = x[0] + 1; printf("%d (sub)\n", x[0]); } 実行結果 2 (sub) 2 (main) ●ポインタの引数 通常の変数でも&演算子を用いることでアドレスが取り出せます。リスト9.6をリスト9.8のようにアドレス渡しに書き換えることができます。アドレスが指すメモリの内容は*演算子で取り出せます。リスト9.8ではsub()内でmain()で宣言された変数iの内容が直接操作されます。 リスト9.8 #include main() { int i; i = 1; sub( &i ); printf("%d (main)\n", i); } sub( int *i ) // iにはアドレスの値が渡される { *i = *i + 1; printf("%d (sub)\n", *i); } [コラム] ソフトウェアとハードウェアとの関係 C言語で利用されるデータやアドレスと、それが実際にやり取りされるコンピュータのハードウェアとの関係を理解しよう。 ○メモリとアドレス データやプログラムを区別するために。”メモリのなかの場所を指定する”ということは、コンピュータを動かす上で重要な作業です。どこのメモリを利用するか、どこのメモリに配置されているプログラムを実行するか、などの情報を管理するためにメモリの1つ1つには番号が振ってあります。この番号をアドレスと呼びます。 C言語ではこのアドレスを利用した変数の操作をポインタというしくみで可能にしています。 図 メモリとアドレス 図 CPUとメモリ間のデータの流れ ●アドレスとポインタ アドレスとポインタは似ていますが、演算方法が異なります。例えば int *p; と宣言すれば、pというポインタ変数が用意され、すでに存在するint型変数iに対して p = &i; を計算すれば変数iの存在するアドレスが取り出せます。このpに対して p = p + 1; を計算すると、pの指す場所はint型変数1個分進めらることになり、int型を32bitで扱うC言語の場合はアドレスの値は4増加します。 ●コンピュータはいかにして暴走するか メモリ上のデータとプログラムは同じ形をしていて、”これはデータ”、”これはプログラム”という分け隔てはありません。CPUは、指定されたメモリの位置から始る一群のデータを勝手に”これはプログラムだな”と解釈して実行します。なにかの拍子に実行を開始すべきプログラムの指定場所を間違えたり、不正にプログラムが書き換えられたりすると、CPUの動作はデタラメなものになります。これを暴走と呼びます。 9.5 main()の引数〜コマンドラインからの入力 コマンドラインからのパラメータを受け取りたいとき、例えば、 A:>TEST ABC.TXT DEF.TXT のABC.TXTとDEF.TXTを受け取りたいとします。main()には2つの引数があり、それを main(int x, char *y[]) とすると、x は 実行ファイル名を含めた引数の数、3 が入ります。yは文字列を指すポインタの配列で、y[0] は実行ファイルのフルパス名を指すアドレスが入ります。y[1] は ABC.TXT を指すアドレスが入り、y[2] は DEF.TXT を指すアドレスが入ります。 簡単なプログラムにしてみましょう。「コマンドラインから2つの数字をパラメータとして読み込みその和をもとめる」プログラムを考えます。 最初の行で、関数mainの引数(xとy)が定義されています。これは、コマンド行の引数とも呼ばれ、実行するときのコマンドに引き続く文字列を引数とします。プログラムの実行開始時にプログラムに対していくつかのパラメータを渡すことができます。この引数の最初の変数xは、コマンド行の文字列の個数を示すもので、その後の文字列へのポインタの配列yの各要素は文字列を指します。atoi()関数は文字列で与えられる数字をint型の数に変換します。 リスト9.10 /* caddn.c */ int main( int x, char *y[] ) { int a,b; if(x != 3) //引数がないとそのまま終了 return 0; a = atoi(y[1]);b = atoi(y[2]); printf("%d\n",a+b); return 1; } リスト9.10の実行結果 z:>caddn 10 20 30 ●main()関数 形式: int main(int argc, char **argv) ・main関数はプロトタイプ宣言(int)をしなくてもかまいません。 ・argc にパラメータの数が入ります。 ・argv[引数番号] にパラメータのポインタが入ります。 ・main()関数は必ずプログラムの始めに実行され、関数が終わればプログラムが終了します。 9.4 入出力関数 これまで暗黙的に引数を利用してきた入出力関数をおさらいします。 ●printf() 形式: int printf(const char *format, ...); 引数: printf は、format によって指される書式文字列中の書式指定を、format の後に続く各 引数に適用し、書式化されたデータを標準出力、パソコンの場合は画面に出力します。 戻り値: 出力したバイト数を返し、エラーの場合はEOF を返します。EOFはstdio.hの中で定義された定数で、 たいていのC言語処理系の場合 -1です。 printf()関数は任意の個数の引数を持つことのできる関数です。その1番目の引数は表示の書式を表し、2番目以降は指定された書式で表すべきデータです。1番目の引数で渡す書式は文字列で記述し、その文字列の先頭アドレスを渡します。例えば printf("abc"); は abc という文字列をメモリ上に用意してその先頭アドレスをprintf()に渡すことになります。ですから。ポインタ変数で文字列の先頭アドレスを中継することもできます。 例: char *p; p = "abc"; printf( p ); ●scanf() 形式: int scanf(const char *format, ...); 引数: scanf は、一連の入力フィールドをスキャンして、一度に1文字ずつストリーム stdin から文字 を読み込みます。次に、引数 format によって指される書式文字列中の書式指定にしたがって、 各フィールドを書式化します。 scanf()はprintf()の逆の動作をする関数です。scanf()はgetch()と違い入力された内容を関数の値として返すのではなく、入力内容を格納すべき場所を2番目以降の引数で指定します。 例: scanf( "%d", &i ); ●フォーマット指定 printf()やscanf()では1番目の引数で入出力される内容の書式を指定します。代表的な指定方法は以下のようです。 書式: % [flags] [width] type ┌───────────────────────────────────────────┐ │ type : 出力のフォーマット │ ├─────────────────────┬─────────────────────┤ │ d 符号つき 10 進法の整数 │ e 浮動少数点 [-]d.ddd e [+/-]ddd │ │ i 符号つき 10 進法の整数 │ E 浮動少数点 [-]d.ddd E [+/-]ddd │ │ o 符号なし 8 進法の整数 │ c 一文字 │ │ u 符号なし 10 進法の整数 │ s 文字列出力。'\0' まで出力 │ │ x 符号なし 16 進法の整数の子文字 │ % % 文字 │ │ X 符号なし 16 進法の整数の大文字 │ p ポインタ(8桁16進表示) │ │ f 浮動少数点 [-]dddd.ddd │ │ ├─────────────────────┼─────────────────────┤ │ [flag] : 左詰め、符号の指定(省略可) │ [width] : 出力桁数の指定(省略可) │ ├─────────────────────┼─────────────────────┤ │ - スペースで埋めて、左へ揃えます。 │ n 少なくとも n 文字あり、 │ │ + いつも + または - で始まります。 │ ブランクで左を埋める │ │ │ 0n 少なくとも n 文字あり、 │ │ │ 0 で左を埋める │ └─────────────────────┴─────────────────────┘ ●書式指定の例 よく使う書式指定の例を示します。 リスト9.9 #include main() { int i=123,j=45; double x=12345.6789, y=1.2; printf( "%d_%f_\n", i, x ); printf( "%d_%f_\n", j, y ); printf( "%10.3f_\n" , x ); // 全体で10桁、小数点以下3桁 printf( "%10.3f_\n" , y ); printf( "%-5d_%+5d_\n", i, i ); printf( "%e_%E_\n", x, y ); printf( "%4.3e_%4.3E_\n", x, y ); printf( "%p_%p_%p_%p_\n", &i, &j, &x, &y ); } 実行結果 123_12345.678900_ 45_1.200000_ 12345.679_ 1.200_ 123 _ +123_ 1.234568e+004_1.200000E+000_ 1.235e+004_1.200E+000_ 0065FDF4_0065FDF0_0065FDE8_0065FDE0_