まずはプログラミングの復習から始めましょう。
1.1 Hello ESYS (ひどい例)
まずは計算機序論1までの復習がてら、ウォーミングアップです。
以下のサンプルは「悪い例」です。こんな書き方してきた人は反省。
何が悪いのでしょうか?
次のサンプルを見る前に、少なくとも4つは指摘できるべきですね。
何が悪かったのでしょうか?
次の改善されたサンプルを見てみましょう。
違いがわかりますか?
コマンドラインでのコンパイル方法も、今後は
$ gcc -Wall -o ターゲット名 ソースCファイル
$ gcc -Wall -o ic2-week1-HelloESYS-Better ic2-week1-HelloESYS-Better.c |
で実行するようにしてください。
少し発展レベルになりますが、より丁寧に記述するなら、下記のように書いておくのもよいでしょう。
1.4 発展:使うと便利なマクロ
C言語プリプロセッサによるマクロについてはすでに習っていると聞いてます。
自分で定義して使うのはもちろんOKですが、Cコンパイラの仕様でもとから定義されている便利なマクロもあります。
標準入出力だけでは、プログラムに対する入力・出力に限界があります。
ここでは、ファイルからの入力・出力ができるようにするための方法について説明します。
ただし、時間の都合で、ファイルからの実際のデータ入力はまたの機会になります。
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以外を返すようにしましょう。
2.3 bashとの連携
main()関数が整数値を返すことになっています。
これは、実はプログラムをshellから呼び出すときに便利です。
2.2で作成したプログラムは、正常終了なら0を、異常終了ならそれ以外を返すようになっています。
これに対して、bashでは、実行終了時のプログラム返値を見て、挙動を変えられる結合子があります。
それが "&&" と "||"です。
"&&" は直前のコマンドが0を返したときのみ次のコマンドを実行、 "||" は逆に直前のコマンドが0以外を返したときのみ次のコマンドを実行します。
";" だと直前のコマンドの結果に関係なく次のコマンドを実行しますので、この辺を組み合わせると結構たくさんの作業が捗るようになります。
さて、計算機序論2では大規模なプログラム作成を体験してもらいます。
単一のファイルでソースを書くことには限界があります。
分割してCプログラムソースを書く技術を身につけてください。
これはあとの演習で必須の技術になります。
3.1 標準入力(コマンドラインからの数値入力)
さて、大きなプログラムに向かう前に、計算機序論2より前の授業の復習を。
標準入力から、数値を1つ頂いてくるプログラムを書いてみてください。
書けましたか?
上記のプログラムは最低限の仕様を満たしていますが、よいプログラムとは言えません。
・"234"と入力したら?
・"234.56"と入力したら?
・"234abc"と入力したら?
・"abc234"と入力したら?
それぞれ何が起きるでしょうか?
特に"abc234"では予想外のことが起きてるかもしれません。
が、それは予想外ではありません。C言語の仕様(約束)を忘れてるから予想外に見えているだけなのです。
3.2 ちょっと大きなプログラム;悪い書き方
入力された数字を、「自乗」したり「2倍」したり「3で割れるか調べ」たり「桁を倍にして表示」したりするプログラムを頼まれたと仮定しましょう。
まずは自分で書いてみてください。
書けましたか?
main関数だけのプログラムとか書かない。恥ずかしいから。いい?
main関数だけのプログラムなど、章分けがないどころか、改行すらないような小説みたいなものです。
短い文章ならそれでも構いませんが、長文では読むに耐えません。
書いている本人も含めて、だれもそのようなプログラムを理解することはできません。
以下はいかにもどこかで見つかりそうな悪い例です。
ic2-week1-Calculations-NotSoGood.c
3.3 ちょっとおおきなプログラム;よい書き方
プログラムを構造化するには、C言語では関数を使うことになります。
目安は、細かいことを見てしまう前に、大局的に考えることです。
(プログラムに限らず小説でも企画書でも報告書でも同じこと。)
今回は5つの作業をするように指示されてますから、5つに作業を分けるのが普通でしょう。
このとき、できるだけ大域変数を使わないようにすることがポイントです。
大域変数を使わずに済めば済むほど、それぞれの関数において、他の関数に影響を受けずに作業を進めることができます。
ic2-week1-Calculations-Functions.c
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 |
$ 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 ./AutoCompile.bash |
こうして分割したCソースファイルのうち、全然書き換えないで使い続ける関数群については、そのうち、「ソース」ではなく、コンパイル済みの状態で保存しておくほうがコンパイルの手間が省けるだろう、という考えがでてきます。
これを推し進めたのが「ライブラリ」です。
今回の例では、5つの作業全てをライブラリ化することを考えます。
つまり、ライブラリ化の対象は下記の4ファイルということになります。
ic2wk1-MyFuncLib-Input.c
ic2wk1-MyFuncLib-Square-And-Double.c
ic2wk1-MyFuncLib-Divide3.c
ic2wk1-MyFuncLib-RepeatTwice.c
ライブラリを使う人(未来への自分を含みます)がコンパイル時にコンパイラにどのような関数を使っているかを示すことが必要になるので、ライブラリ内の関数群の型をまとめたファイルを用意しておきます。これがheaderファイルと呼ばれるものです。
ヘッダファイルにはライブラリ中で書いた(用意した)関数の型は書いてあっても、機能説明はありません。
そのため、機能説明に関しては、未来の自分に向けてメモを書いておく必要があります。
(ちなみにこの説明が大規模化していってできたのが 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行ずつ改行して表示すること。
・以下は例なので、必ずしもこの形式でなくともよい。
・日本語環境は考慮しなくてよい(英数文字のみとする)。
出力例 |
$ ./hissukadai1a 12345.67 Hello Esys_work
argc = 4 argv[0] = "./hissukadai1a" argv[1] = "12345.67" argv[2] = "Hello" argv[3] = "Esys_work" |
提出物
分割コンパイル時に、main()関数を記述したファイル(ic2-week1-Calculations-Main.c)側では、他の5つの関数の型の定義を記述しておく必要がある。
(1B-1) この記述がないと、何がどうまずいのか説明せよ。
(1B-2) もしic2-week1-Calculations-Main.c内の関数の型の定義と、実際の型の定義が異なっていた場合、どの時点でどういう不都合が起きるか説明せよ。
提出物
標準入力から、まず入力すべき整数の個数を指定し、続いてその個数だけ整数の入力を受付て表示するプログラムを作成せよ。
・整数値として無効な入力があった場合は、即座にプログラム実行を打ちきってよい。
・ユーザからの整数入力は1度には(1行)には1つまでと仮定してよい。
・入力として受け取る整数値を主記憶上に保存する必要はない(すぐ表示すればそれでよい)
・課題提出の制約上(ファイル1つのみ)、ライブラリ利用やソースファイル分割をせずに、単一ファイルのみで作成すること。
なお、さらに余裕のある者は以下に挑戦してもよい。
・整数値以外の入力がきた場合は、再度入力を促す。
提出物
(1Y-1) 課題1Aのプログラムによる出力例において、引数を分ける文字として空白(スペース)がある。
では、空白を含む文字列を1つの引数としたい場合は、どのように指定すればよいか、その指定方法を示せ。
(1Y-2) 課題1Xのプログラムによる入力において、1行に複数の数字を(間にスペースを入れて)記述したらプログラムはどのように動作するか。
その理由をプログラムコードとともに説明せよ。
1Y-1の出力例 |
$ ./hissukadai1a ?????????
argc = 2 argv[0] = "./hissukadai1a" argv[1] = "Esys work" |
提出物