2. ファイルの開き方

【プログラミング序論D】 目次, 講義ページ, 授業科目, www.kameda-lab.org 2018/01/16a

ファイルのオープンとクローズは,基本技術の一つです.

プログラムの外の存在であるファイルは,実際にはなかったりすることもありますから,エラートラップは重要です.
エラーが起きることを予め予測して対処法を考えておくのが良いプログラムの書き方です.
C言語では,エラー処理は(残念ながら)自分で行うのが基本です.
(それゆえにエラーを敢えて無視するという高等戦術を取れるというメリットもあるのですが‥)

ユーザの希望するファイルをコマンドラインからプログラムに与えられるようにするために,main()関数の引数についても合わせてここで学習します.


02.01. ファイルのぞんざいな開き方

ファイルのオープンには,fopen()関数を使います.
manでfopenを調べると,fopen()関数を使いたいなら,"#include <stdio.h>"を予め書いておきなさいと指示があるので,ソースの最初のほうに記入しておきます.
(実際にはいつも使っているprintf()が同じことをすでに要求しているので,2回書く必要はありません.)

fopen()は,対象となるファイル名と開き方(ここでは読込をしたいという意思表示のために"r"を指定しています)を指定して使います.
(fopen()の第2引数についてもmanページを見ましょう)
オープンに成功すると対象ファイルに対応した「番号札(FILE構造体へのポインタ)」を渡してもらえます.
(正式にはファイルディスクリプタと呼ぶものです.)
以後はその番号札を用いてデータ入出力を行います.
使用後,ファイルのクローズにはfclose()関数を用います.
(そんな開き方で大丈夫か?大丈夫だ,問題ない...と言えますか?)

標準入出力を使うときは,こうした作業は不要でした.
これは,コンパイラが皆さんに黙って作業していてくれたからです.
コンパイラは,プログラム実行時・終了時に「標準入出力に対するオープン」「標準入出力に対するクローズ」の操作を実行できるように,そういう機能の関数を,皆さんの書いたプログラムの前後にこっそり付け加えてコンパイルしてくれていたのでした.

02-01-OpenFile-NotSoGood.c

ポイントは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バイトの領域の先頭バイトアドレス(勝手に数値にしてくれたりはしません)
という状態になっています.
(それぞれ1バイト多いような気がするのは,先方が文字列の最後に文字列終端を表す'\0'をくっつけてくれているからです)

上記のプログラムは,こうした必要最低限のことは確かに出来ていますが,いい書き方とは言えません.
どうまずいか,実際にやってみましょう.
$ 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-01-ex2: UNIXコマンド chmod の一般的な使い方を本節の利用に合わせた形で説明しなさい.

02-01-ex3: プログラム中に "#include <stdio.h>" を2回以上書いたらどうなりますか?
(ヒント:04.02.節で復習します)


02.02. ファイルの上品な開き方

下のプログラムは,様々な状況が来ても対応できるようになっているプログラムです.

02-02-OpenFile-Full.c
02-01からの差分

ユーザが,いつも存在するファイル名だけを指定してくれる(上記の「まともに動く例」)とは限りません.

そのための対応をするべきです.
実は,大抵の場合,皆さんが使うライブラリ関数は,期待通りに動いている(動けた)のかどうかを返値で教えてくれます.
その値を上手に使えばよいのです.
今後も,新しく出てくるライブラリ関数については,その使用法や返値の意味を,manページを参照して確認してください.
(ここからは今までのようにいちいちmanページをこちらで用意しませんから.)
(ググってもよし.ただしWindows用とかでなくUnix用の正しいページを探さないと混乱のもとですよ.google先生は万能ではありません.)

ここで,コンピュータプログラマの不文律として,整数の返値が0だと「期待通り;平穏無事」と解釈する,というものがあります.
皆さんがこの先,C言語で自分で関数を作成して返り値で状態を返すときは,整数型にして正常終了を0に,それ以外の異常終了時には0以外を返すようにしましょう.
(これは「文化的な慣習」なので,こういう習慣のないプログラマもたくさんいますけどね.)

また,ファイルに対して用がなくなったら,速やかにfclose()でファイルを閉じる習慣をつけておきましょう.
(整理整頓は将来のトラブルを未然に防いでくれますよ!)

演習

02-02-ex1: 手慣れたプログラマが書いたCプログラムでは,整数を返値とする関数について,正常終了時は0を,異常終了時には-1を返しているものが多い.
・これには一応理由があります.調べて説明を試みなさい.
(ヒント:2の補数表現)

02-02-ex2: 皆さんがCプログラムから実行ファイルを作って実行した場合,同時に開けるファイル数には上限があります.
・「同時に」の意味を正確に表現して下さい.
・その上限数を,調査方法と共に示しなさい.
(私は昔々,これで実際に痛い目に遭いました‥当時のOSはこの数が意外なほど小さくてですねぇ‥)


02.03. bashとの連携

ところで,なんでmain()関数は整数値を返すことになっているのでしょうね?
それは,プログラムをターミナル(厳密にはそこで動いているbashというshell)から呼び出すときに便利だからです.
本授業で使うUNIX計算機では,ターミナルではbashというshellが使われていて,OSと皆さんの入出力との間を取り持っています.
(打ち込まれた文字をOS/プログラムに渡しに行ったり,OS/プログラムからの文字列を画面に表示したりしています)
実はbashのコマンドラインで動かすプログラムやコマンドは,全て整数値を返値としてもつようにしなさい,という取り決めがあります.
(bash も UNIXコマンドですから,man ページでその機能を確認できます)

02.02.節で作成したプログラムは,正常終了なら0を,異常終了ならそれ以外を返すようになっています.
これに対して,bash側で,実行終了時のプログラム返値を見て,挙動を変えられる制御演算子という演算子があります.
それが "&&" と "||"です.
"&&" は直前のコマンドが0を返したときのみ次のコマンドを実行, "||" は逆に直前のコマンドが0以外を返したときのみ次のコマンドを実行します.
";" だと直前のコマンドの結果に関係なく次のコマンドを実行しますので,この辺を組み合わせると結構たくさんの作業が捗るようになります.
(man bashのListsというセクションに説明が書いてあります)

02-03-OpenFile-AndGo.bash

中に書いてあるコマンドを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を示すこと.


kameda[at]iit.tsukuba.ac.jp