計算機序論2(2011年度)実習1週目(2011/09/22)

計算機序論2, 授業科目, www.kameda-lab.org 2011/10/24b

1. Hello World ならぬ Hello ESYS

まずはプログラミングの復習から始めましょう。

1.1 Hello ESYS (ひどい例)

まずは計算機序論1までの復習がてら、ウォーミングアップです。
以下のサンプルは「悪い例」です。こんな書き方してきた人は反省。

ic2-week1-HelloESYS-Simple.c

何が悪いのでしょうか?
次のサンプルを見る前に、少なくとも4つは指摘できるべきですね。


1.2 Hello ESYS をまともな形にしてみよう

ic2-week1-HelloESYS-Simple.c

何が悪かったのでしょうか?
次の改善されたサンプルを見てみましょう。

ic2-week1-HelloESYS-Better.c

違いがわかりますか?

コマンドラインでのコンパイル方法も、今後は
$ gcc -Wall -o ターゲット名 ソースCファイル
$ gcc -Wall -o ic2-week1-HelloESYS-Better ic2-week1-HelloESYS-Better.c

で実行するようにしてください。


1.3 Hello ESYS をさらに丁寧に書いてみたら?

少し発展レベルになりますが、より丁寧に記述するなら、下記のように書いておくのもよいでしょう。

ic2-week1-HelloESYS-Full.c


1.4 発展:使うと便利なマクロ

C言語プリプロセッサによるマクロについてはすでに習っていると聞いてます。
自分で定義して使うのはもちろんOKですが、Cコンパイラの仕様でもとから定義されている便利なマクロもあります。

ic2-week1-HelloESYS-MACROs.c


2. ファイルからの読み込み

標準入出力だけでは、プログラムに対する入力・出力に限界があります。
ここでは、ファイルからの入力・出力ができるようにするための方法について説明します。
ただし、時間の都合で、ファイルからの実際のデータ入力はまたの機会になります。


2.1 ファイルを開いて(将来、データを読む準備)あまりよろしくない例

ファイルのオープンには、対象となるファイル名を指定してfopen()を使います。
オープンに成功すると対象ファイルに対応した「番号札(FILE構造体へのポインタ)」を渡してもらえます。
以後はその番号札を用いてデータ入出力を行います。
使用後、ファイルのクローズにはfclose()関数を用います。

標準入出力を使うときは、こうした作業は不要でした。
これはプログラム実行時・終了時に「標準入出力に対するオープン」「標準入出力に対するクローズ」の操作をするように、コンパイラが皆さんに黙ってそういう関数を皆さんの書いたプログラムの前後に付け加えていたからです。
特定のファイルをユーザが使いたくなってくると、コンパイラ側では予想のしようもないので、ユーザが自分でこれらの操作をする必要が出てきます。

ic2-week1-ReadFile-NotSoGood.c

上記のプログラムは、こうした必要最低限のことは確かに出来ていますが、いい書き方とは言えません。
どうまずいか、実際にやってみましょう。
$ gcc -Wall -o ic2-week1-ReadFile-NotSoGood ic2-week1-ReadFile-NotSoGood.c
$ emacs abc.txt (なにか作成してみましょう。今回は中身は空でも構いません。)
$ ls (少なくとも ic2-week1-ReadFile-NotSoGood, ic2-week1-ReadFile-NotSoGood.c, abc.txtはあるはず)
$ ./ic2-week1-ReadFile-NotSoGood abc.txt (まともに動く例)
$ ./ic2-week1-ReadFile-NotSoGood
$ ./ic2-week1-ReadFile-NotSoGood def.txt
$ chmod u-r abc.txt
$ ./ic2-week1-ReadFile-NotSoGood abc.txt
$ chmod u+r abc.txt
$ ./ic2-week1-ReadFile-NotSoGood abc.txt

何回セグメンテーションフォールトを見ましたか?その理由が分かりますか?
これらのセグメンテーションフォールトは行儀のよいプログラムを書けば(正しく美しいC言語を話せば)すべて回避できます。


2.2 ファイルを開いて(将来、データを読む準備)まっとうな書き方

下のプログラムは、様々な状況が来ても対応できるようになっているプログラムです。
ユーザがまともにファイルを指定してくれる(上記の「まともに動く例」)とは限りません。
皆さんが使うシステム関数は、実は期待通りに動いている(動けた)のかどうかを大抵の場合、返値で教えてくれます。
その値を上手に使えばよいのです。
それぞれのシステム関数の返値の意味は、manページを参照してください(ないしはググってもよし。ただしWindows用とかでなくUnix用を探すこと)。
ここで、コンピュータプログラマの不文律として、整数の返値が0=期待通り;平穏無事というものがあります。
皆さんがこの先、自分で関数を作成して返り値で状態を返すときは、正常終了を0に、それ以外の異常終了時には0以外を返すようにしましょう。

ic2-week1-ReadFile-Full.c


2.3 bashとの連携

main()関数が整数値を返すことになっています。
これは、実はプログラムをshellから呼び出すときに便利です。
2.2で作成したプログラムは、正常終了なら0を、異常終了ならそれ以外を返すようになっています。
これに対して、bashでは、実行終了時のプログラム返値を見て、挙動を変えられる結合子があります。
それが "&&" と "||"です。
"&&" は直前のコマンドが0を返したときのみ次のコマンドを実行、 "||" は逆に直前のコマンドが0以外を返したときのみ次のコマンドを実行します。
";" だと直前のコマンドの結果に関係なく次のコマンドを実行しますので、この辺を組み合わせると結構たくさんの作業が捗るようになります。

ic2-week1-ReadFile-AndGo.bash


3. 大きなプログラムへの対応;関数の利用

さて、計算機序論2では大規模なプログラム作成を体験してもらいます。
単一のファイルでソースを書くことには限界があります。
分割してCプログラムソースを書く技術を身につけてください。
これはあとの演習で必須の技術になります。


3.1 標準入力(コマンドラインからの数値入力)

さて、大きなプログラムに向かう前に、計算機序論2より前の授業の復習を。
標準入力から、数値を1つ頂いてくるプログラムを書いてみてください。

ic2-week1-STDIN.c

書けましたか?
上記のプログラムは最低限の仕様を満たしていますが、よいプログラムとは言えません。
・"234"と入力したら?
・"234.56"と入力したら?
・"234abc"と入力したら?
・"abc234"と入力したら?
それぞれ何が起きるでしょうか?
特に"abc234"では予想外のことが起きてるかもしれません。
が、それは予想外ではありません。C言語の仕様(約束)を忘れてるから予想外に見えているだけなのです。


3.2 ちょっと大きなプログラム;悪い書き方

入力された数字を、「自乗」したり「2倍」したり「3で割れるか調べ」たり「桁を倍にして表示」したりするプログラムを頼まれたと仮定しましょう。
まずは自分で書いてみてください。

書けましたか?
main関数だけのプログラムとか書かない。恥ずかしいから。いい?
main関数だけのプログラムなど、章分けがないどころか、改行すらないような小説みたいなものです。
短い文章ならそれでも構いませんが、長文では読むに耐えません。
書いている本人も含めて、だれもそのようなプログラムを理解することはできません。

以下はいかにもどこかで見つかりそうな悪い例です。

ic2-week1-Calculations-NotSoGood.c


3.3 ちょっとおおきなプログラム;よい書き方

プログラムを構造化するには、C言語では関数を使うことになります。
目安は、細かいことを見てしまう前に、大局的に考えることです。
(プログラムに限らず小説でも企画書でも報告書でも同じこと。)
今回は5つの作業をするように指示されてますから、5つに作業を分けるのが普通でしょう。

  1. 数字を入力してもらって受け取る
  2. 「自乗」したり
  3. 「2倍」したり
  4. 「3で割れるか調べ」たり
  5. 「桁を倍にして表示」したり

このとき、できるだけ大域変数を使わないようにすることがポイントです。
大域変数を使わずに済めば済むほど、それぞれの関数において、他の関数に影響を受けずに作業を進めることができます。

ic2-week1-Calculations-Functions.c


4. 大きなプログラムへの対応;分割したファイルでのプログラミング

3.で見たように、かわいいサイズのプログラム以外は、基本は複数の関数に分けてプログラムコードを書きます。
そうしたほうが作業効率がいいからです。
そうすると、そのうち、幾つかの関数は昔に書いたきり、使うだけで変更とか一切しなくなったりします。
こういう昔書いて、もう変更しない関数は、別ファイルに分けておくと、事故(誤って書き換えてしまう)を防ぐことができます。

3.3の ic2-week1-Calculations-Functions.c に出てきていた関数群を、main関数の入っているファイルと別のファイルの分けて書くようにしてみましょう。
今回は、「自乗」と「2倍」する関数だけは1つのファイルにまとめてみたので、main関数のファイルと併せて合計5つのファイルに分けます。

ic2-week1-Calculations-Main.c (main関数側)

以下は分割された側
ic2-week1-Calculations-Sub-Input.c 「数値入力」
ic2-week1-Calculations-Sub-Square-And-Double.c 「自乗」「2倍」
ic2-week1-Calculations-Sub-Divide3.c 「3で割りきれるか?」
ic2-week1-Calculations-Sub-RepeatTwice.c 「桁を倍にして表示」

main関数側の、main()の直前に残された5行に注目してください。
他のソースは全て分割された側に送り出されましたが、それぞれの関数の型だけは、main関数のファイルに残してあります。
コンパイラがmain()内部の文法で整合が取れてるかどうか確認ができるようにするためです。

コンパイルは、2段階に分かれます。
1段目は、各ソースをオブジェクトファイルに変換する狭義の「コンパイル」です。
上記5ファイルとも、下記と同じ要領で(狭義の)コンパイルします。

$ gcc -Wall -c ic2-week1-Calculations-Main.c

2段目は、オブジェクトファイルを全て結合して実行ファイルを作成する「リンク」です。
これは結合予定のオブジェクトファイルを全て準備してから行います。
gccの-oオプションで希望する実行ファイル名を指定したあと、リンク予定のオブジェクトファイルを全て並べます。
(改行せずに全部1行で入力すること)

$ gcc -o ic2-week1-Calculations-A ic2-week1-Calculations-Sub-Input.o ic2-week1-Calculations-Sub-Square-And-Double.o ic2-week1-Calculations-Sub-Divide3.o ic2-week1-Calculations-Sub-RepeatTwice.o ic2-week1-Calculations-Main.o

コンパイルがだんだん面倒になってきたので、bashのスクリプトを用意してみました。
コマンドラインで打つ内容は、この様にテキストファイルにしておくことで、全て一気にbashに実行させることができます。
ただし、打ち間違えると大変なことになったりするので注意。

$ bash ./AutoCompile.bash


5. 大きなプログラムへの対応;ライブラリ化

こうして分割したCソースファイルのうち、全然書き換えないで使い続ける関数群については、そのうち、「ソース」ではなく、コンパイル済みの状態で保存しておくほうがコンパイルの手間が省けるだろう、という考えがでてきます。
これを推し進めたのが「ライブラリ」です。

今回の例では、5つの作業全てをライブラリ化することを考えます。
つまり、ライブラリ化の対象は下記の4ファイルということになります。

ic2wk1-MyFuncLib-Input.c
ic2wk1-MyFuncLib-Square-And-Double.c
ic2wk1-MyFuncLib-Divide3.c
ic2wk1-MyFuncLib-RepeatTwice.c

ライブラリを使う人(未来への自分を含みます)がコンパイル時にコンパイラにどのような関数を使っているかを示すことが必要になるので、ライブラリ内の関数群の型をまとめたファイルを用意しておきます。これがheaderファイルと呼ばれるものです。

ic2wk1-MyFuncLib.h

ヘッダファイルにはライブラリ中で書いた(用意した)関数の型は書いてあっても、機能説明はありません。
そのため、機能説明に関しては、未来の自分に向けてメモを書いておく必要があります。
(ちなみにこの説明が大規模化していってできたのが man ページです。)

さて、ここでライブラリ libMyFunc.a を実際に作ってみましょう。
コマンドをいちいち書くのが面倒なので、シェルスクリプトを用意しました。

$ bash ./AutoLibBuilder1.bash $ ar -t libmyFunc.a (libMyFunc.aの中身の確認)

こうして libMyFunc.a というライブラリができると、あとはそれを利用して様々なプログラムを書いてみることができます。
headerファイルは、カレントディレクトリにそれがある場合はダブルクォートでファイル名を囲みます。

$ gcc -Wall -o ic2-week1-Example1 ic2-week1-Example1.c -I. -L. -lMyFunc
$ gcc -Wall -o ic2-week1-Example2 ic2-week1-Example2.c -I. -L. -lMyFunc

-Iオプションはヘッダファイルを探索すべきディレクトリを、-Lオプションはライブラリを探索すべきディレクトリをそれぞれ示します。
ここではどちらも (ic2wk1_MyFuncLib.h と libMyFunc.a) カレントディレクトリにあると仮定しています。
-lMyFuncを指定することで、実際には libMyFunc.a ライブラリを指定したことになります。

実は、この授業では、今後ライブラリを実際に作ることは予定していません。
ただ、あとで外部ライブラリを利用することが出てきますので、概念に馴染んでいてもらうために演習としては用意しました。


課題1. 2011/09/22出題分


kameda[at]iit.tsukuba.ac.jp, 2011.