ファイルのオープンとクローズは、基本技術の一つです。
プログラムの外の存在であるファイルは、実際にはなかったりすることもありますから、エラートラップは重要です。
エラーが起きることを予め予測して対処法を考えておくのが良いプログラムの書き方です。
C言語では、エラー処理は(残念ながら)自分で行うのが基本です。
(それゆえにエラーを敢えて無視するという高等戦術を取れるというメリットもあるのですが‥)
ユーザの希望するファイルをコマンドラインからプログラムに与えられるようにするために、main()関数の引数についても合わせてここで学習します。
ファイルのオープンには、fopen()関数を使います。
manでfopenを調べると、使うためには"#include <stdio.h>"を予めしておきなさいと書いてあるので、ソースの最初のほうに記入しておきます。
(実際にはいつも使っているprintf()が同じことをすでに要求しているので、2回書く必要はありません。)
fopen()は、対象となるファイル名と開き方(ここでは読込をしたいという意思表示のために"r"を指定しています)を指定して使います。
(fopen()の第2引数についてもmanページをみましょう)
オープンに成功すると対象ファイルに対応した「番号札(FILE構造体へのポインタ)」を渡してもらえます。
(正式にはファイルディスクリプタと呼ぶものです。)
以後はその番号札を用いてデータ入出力を行います。
使用後、ファイルのクローズにはfclose()関数を用います。
(そんな開き方で大丈夫か?大丈夫だ、問題ない...と言えますか?)
標準入出力を使うときは、こうした作業は不要でした。
これはプログラム実行時・終了時に「標準入出力に対するオープン」「標準入出力に対するクローズ」の操作をするように、コンパイラが皆さんに黙ってそういう機能の関数を、皆さんの書いたプログラムの前後に付け加えて記述し、それをまとめてコンパイルしてくれていたからです。
ポイントはmain()関数の引数である argc と argv です。
argcから、コマンドラインからの実行時の引数の数が分かります。この数には実行ファイルの名前自身も含みます。
argv[]は文字列ポインタの配列です。この配列はargc個分(0〜argc-1)用意してくれてあります。
一つの文字列ポインタを参照すると、そこから'\0'の入ったバイトまで文字列が続いているようになっています。
例えば
$ ./02-01-OpenFile-NotSoGoodとすると、実行開始時にOSからユーザプログラムのmain関数が呼ばれ、そのときに、
・argc は 1 ・argv[0] は "./02-01-OpenFile-NotSoGood" という文字列が入った27バイトの領域の先頭バイトアドレスという状態になっています。
また、別の例であれば、
$ ./02-01-OpenFile-NotSoGood abc.txt 2ndoption.log 2011とすると、
・argc は 4 ・argv[0] は "./02-01-OpenFile-NotSoGood" という文字列が入った27バイトの領域の先頭バイトアドレス ・argv[1] は "abc.txt" という文字列が入った8バイトの領域の先頭バイトアドレス ・argv[2] は "2ndoption.log" という文字列が入った14バイトの領域の先頭バイトアドレス ・argv[3] は "2011" という文字列が入った5バイトの領域の先頭バイトアドレス(勝手に数値にしてくれたりはしません)という状態になっています。
上記のプログラムは、こうした必要最低限のことは確かに出来ていますが、いい書き方とは言えません。
どうまずいか、実際にやってみましょう。
$ gcc -Wall -o 02-01-OpenFile-NotSoGood 02-01-OpenFile-NotSoGood.c
$ emacs abc.txt (なにか作成してみましょう。今回は中身は空でも構いません。) $ ls (少なくとも 02-01-OpenFile-NotSoGood, 02-01-OpenFile-NotSoGood.c, abc.txtはあるはず) $ ./02-01-OpenFile-NotSoGood abc.txt (まともに動く例) $ ./02-01-OpenFile-NotSoGood (落ちます) $ ./02-01-OpenFile-NotSoGood def.txt (落ちます) $ chmod u-r abc.txt (ユーザ自身ですらabc.txtを読めない状態にすると‥) $ ./02-01-OpenFile-NotSoGood abc.txt (abc.txtを指定しても落ちるようになります) $ chmod u+r abc.txt (ユーザ自身でabc.txtを読めるように戻すと‥) $ ./02-01-OpenFile-NotSoGood abc.txt (まともに動くように戻ってきました) |
何回セグメンテーションフォールトを経験しましたか?その理由が分かりますか?
これらのセグメンテーションフォールトは行儀のよいプログラムを書けば(正しく美しいC言語を書けば)すべて回避できます。
また、皆さんはこうしたクリティカルエラーを回避すべきです。
演習
02-01-ex1: 本節の説明に即した形で、関数型プログラミング言語の特徴を述べよ。
下のプログラムは、様々な状況が来ても対応できるようになっているプログラムです。
02-02-OpenFile-Full.c
02-01からの差分
ユーザが、いつも存在するファイル名だけを指定してくれる(上記の「まともに動く例」)とは限りません。
そのための対応をするべきです。
実は、皆さんが使うライブラリ関数は、期待通りに動いている(動けた)のかどうかを大抵の場合、返値で教えてくれます。
その値を上手に使えばよいのです。
今後も、新しく出てくるライブラリ関数については、その使用法や返値の意味を、manページを参照して確認してください。
(ここからは今までのようにいちいちmanページをこちらで用意しませんから。)
(ググってもよし。ただしWindows用とかでなくUnix用の正しいページを探さないと混乱のもとですよ。google先生は万能ではありません。)
ここで、コンピュータプログラマの不文律として、整数の返値が0=期待通り;平穏無事というものがあります。
皆さんがこの先、C言語で自分で関数を作成して返り値で状態を返すときは、整数型にして正常終了を0に、それ以外の異常終了時には0以外を返すようにしましょう。
(これは「文化的な慣習」なので、こういう習慣のないプログラマもたくさんいますけどね。)
また、ファイルに対して用がなくなったら、速やかにfclose()でファイルを閉じる習慣をつけておきましょう。
(整理整頓は将来のトラブルを未然に防いでくれますよ!)
演習
02-02-ex1: 手慣れたプログラマが書いたCプログラムでは、整数を返値とする関数について、正常終了時は0を、異常終了時には-1を返しているものが多い。
・これには一応理由があります。調べて説明を試みなさい。
(ヒント:2の補数表現)
ところで、なんでmain()関数は整数値を返すことになっているのでしょうね?
それは、プログラムをshellから呼び出すときに便利だからです。
実はbashのコマンドラインで動かすプログラムやコマンドは、全て整数値を返値としてもつようにしなさい、という取り決めがあります。
02.02.節で作成したプログラムは、正常終了なら0を、異常終了ならそれ以外を返すようになっています。
これに対して、bash側で、実行終了時のプログラム返値を見て、挙動を変えられる結合子があります。
それが "&&" と "||"です。
"&&" は直前のコマンドが0を返したときのみ次のコマンドを実行、 "||" は逆に直前のコマンドが0以外を返したときのみ次のコマンドを実行します。
";" だと直前のコマンドの結果に関係なく次のコマンドを実行しますので、この辺を組み合わせると結構たくさんの作業が捗るようになります。
(man bashのListsというセクションに説明が書いてあります)
中に書いてあるコマンドを1行ずつ自分でコマンドラインで打ってみてください。
(前提として、実行プログラム名を02-02-OpenFile-Fullであるとしています)
(それが面倒になったら、"$ bash ./02-03-OpenFile-AndGo.bash"でまとめて実行できたりします)
(このようなファイルを「スクリプト」と呼びます。これはbash用なので、「bash script」と呼びます)
(bash script中では、#はコメントの開始を表しています)
(C言語では // とか /* */ なのに、なぜ違うのかって? bashを最初に開発した人たちの趣味ですね、単に)
演習
02-03-ex1: コマンドライン引数の数とその内容を全て表示するプログラムを作成しなさい。
・各引数は1行ずつ改行して表示すること。
・日本語の引数は考慮しなくてよい(英数文字のみとする)。
02-03-ex2: コマンドライン引数の1つとしてスペースを含む文字列を与えるには、コマンドラインでどのように入力すべきか示しなさい。
02-03-ex3: 変数argvの型を正確に答えなさい。
02-03-ex4: コマンド名 "./example"、第1引数 "234.545"、第2引数 "input.log"、として実行プログラムでの起動直後のargvから始まるメモリ構造を図示しなさい。その中の適切な場所にargcを示すこと。