標準入力・標準出力・標準エラー出力

stdin と stdout

ストリームを用いたファイル入出力について色々と学んできました。 scanf を使った標準入力の読み込みや、printf を使った標準出力への書き込みに比べて 少し面倒だと思われたかも知れません。 しかし実は、標準入力や標準出力もストリームの一種なのです。 皆さんは、気づかないうちにすでにストリーム入出力を使っていたのです。

「え、どういうこと?」と思われたかも知れません。 標準入力・標準出力だけを使っていた頃は FILE などという構造体は知らずに済みましたし、 fopen も不要、fprintf 関数も知らずに済んでいたのですから、 標準入力・標準出力もストリームの一種だと突然言われても わけがわからないでしょう。 そこで、これからその謎の種明かしをします。 これを読めば、UNIX の入出力をもっとよく理解できるようになるでしょう。

標準入力・標準出力は最初からオープンされている

標準入力と標準出力は、プログラムの実行が始まったときには すでにオープンされている仕掛けになっています。 (UNIX がそういう仕掛けを用意してくれています。) プログラマが自分でオープンする必要はありません。 これが、標準入力・標準出力を使うのに fopen が必要でなかった理由です。

標準入力が最初にオープンされたときにできた入力ストリームは、 大域変数 stdin に代入されています。これは読み出し専用のストリームです。 同様に、標準出力のストリームは、大域変数 stdout に代入されています。 こちらは書き込み専用のストリームです。 scanf 関数や printf 関数は、関数内部でこれらのストリームを使って入出力を行っています。 例えば、printf 関数は fprintf 関数の第1引数に stdout を指定したものと同等です。

stdin と stderr は、ヘッダーファイル stdio.h をインクルードすると、 大域変数として宣言されるようになっています。 これらの変数のためのメモリ領域は libc というライブラリの中で確保されています。

端末もファイルの1つ

kterm のような端末ソフトウェアからプログラムを起動したとき、 プログラムの標準入力及び標準出力は、普通、その端末になっています。 正確に言えば、 リダイレクションやパイプによって標準入力や標準出力が他へつながれていない限り、 入出力先は端末になっています。 標準入力を scanf などで読み出せば、端末から入力が読み込まれますし、 標準出力に printf などで出力をすれば、端末の画面に出力が表示されます。 ところで、さきほど、標準入力・標準出力もストリーム入出力だと言いました。 すると、端末もファイルだということになってしまいます。 しかし、端末をいくらながめてもファイルらしくは見えません。 本当に端末もファイルなのでしょうか?

はい、これはある意味で本当です。 UNIX では端末もファイルの一種として扱われるのです。 UNIX では、 普通ならファイルとは思えないようなものも (仮想的な)ファイルとして扱えるようになっています。 これは UNIX の非常に優れた点で、ファイル入出力の方法を覚えれば、 その知識が様々なものに対する入出力に応用できるのです。 プログラマにとってありがたいことです。

端末がファイルになっているぐらいで驚いてはいけません。 音声入出力の装置や、CD-ROM ドライブ、 果てはマウスからの入力にいたるまで、 あらゆる装置が UNIX ではファイルとして扱われています。 ただし、「では試しに…」ということで音声入出力装置に妙なデータを出力 してみたりすると、ひどい音が出てびっくりするかも知れませんよ。

リダイレクションの仕組み

fopen によってファイルをオープンすると、 FILE 構造体が作られて、ファイルとの間に対応関係ができます。 通常、stdin と stdout は端末に対応しているので、 これらに対して入出力を行えば、端末に対する入出力になります。 しかし、この対応関係は変更が可能です。 端末に対応していたのを、別のファイルに対応するように変更してやれば、 リダイレクションが実現できます。

scanf 関数の危険性

これまで、標準入力からデータを読むときには scanf 関数を使ってきました。 しかし、わけあってこれまでは黙っていたことなのですが、 scanf 関数の使用には危険が伴います。 自分だけが使うようなプログラムの中でなら、 潜在的な危険を承知で使っても良いでしょうが、 真剣な用途のプログラムでは決して scanf 関数を使ってはいけません。 代りに、次のように fgets と sscanf を組み合せて使って下さい:

    fgets(バッファ, バッファのサイズ, stdin);   // 標準入力から1行読み出してバッファに入れる
    sscanf(バッファ, フォーマット文字列, ...);  // バッファ内の文字列からデータを読み取る

このようにすべき理由はいくつかあげられます:

  1. 最大の理由は、scanf がバッファのあふれ(バッファオーバフロー) を考慮しないことです。例えば、 scanf("%s", buffer); を実行すると、文字列が読み込まれて バッファにつめ込まれますが、文字列の長さがバッファのサイズを 越えていても scanf は全くお構いなしにバッファにつめようとします。 その結果、バッファの終端を越えてデータが書き込まれてしまい、 プログラムの動作が狂う可能性があります。 プログラムにこのような欠陥があると、 コンピュータシステムにいたずらをしようとする攻撃者の格好の攻撃目標にされてしまいます。 攻撃者はわざと長い文字列を入力して、プログラムの動作を自分の都合のいいように狂わせるのです。
  2. scanf は指定されたフォーマットのデータが現れるまで 何行でも入力を読み続けてしまうことがあります。 こうなると、何行入力を読んだのかわからなくなってしまいます。 一方、fgets を使って読み込んだ場合は、1行しか読み込みませんから、 どこまで入力を読んだのか、いつも完全に把握していられます。
  3. プログラムが想定しているフォーマットでデータが読み取れなかったとき (ユーザが入力のしかたを間違えるとそうなります)、fgets を使っていれば、 バッファにユーザからの1行分の入力が残っているので、それを分析することで 問題点を発見できます。しかし、scanf を使った場合はそれができません。

それならどうして scanf を教えたのか、と言われそうですが、プログラミング の初心者にいきなり fgets と sscanf を教えるのは困難です。そのため、多く の教科書で scanf が教えられているのが実情ですし、初心者のレベルを越えた 授業でも説明を簡単にするために scanf を用いているのを目にします。しかし、 問題点がわかった以上、実用のプログラムでは決して scanf を使わないようにして下さい。

もちろん、scanf 以外の関数でも、バッファオーバフローの危険があるものは使わないようにしなければなりません。そうした関数の名前をいちいちここに並べることはできませんので、バッファを操作するような関数を使うときは参考書やオンラインマニュアルで危険がないか確かめて使うようにしましょう。

stderr

標準入力と標準出力以外に、もう1つ、最初から開いているストリームがあります。 それが標準エラー出力と呼ばれるもので、書き込み専用のストリームです。

標準エラー出力は、エラーメッセージや警告メッセージを出力するためのストリームで、 大域変数 stderr に代入されています。 kterm のような端末ソフトウェアからプログラムを普通に起動した場合、 標準エラー出力は端末につながっています。 従って、標準エラー出力にメッセージを書き出せば、 メッセージは端末の画面に表示されます。

では、標準エラー出力にメッセージを書き出すにはどうすればよいでしょうか。 ストリーム出力をする関数ならどれでも当然使えますが、 簡単なのは fprintf 関数を用いることでしょう。次のようにすればよいのです:

fprintf(stderr, フォーマット文字列, ...)

例えば、filename という変数がファイル名を指しているとして、 ファイルがオープンできない時のエラーメッセージは

fprintf(stderr, "エラー: ファイルがオープンできません: %s\n", filename);

などとすればよいでしょう。

終了ステータスについて学んだ章で、 exit 関数を用いてプログラムを異常終了させる方法を覚えましたが、 そのような場合、 大抵はエラーメッセージや警告メッセージを出力してから異常終了するのが適切です。 stderr について学んだので、これからは、標準エラー出力にエラーメッセージを出してから exit するようにしましょう。例えば、次のような書き方になります:

  inputfile = fopen(argv[1], "r");    // ファイルを読み出し用にオープン
  if (inputfile == NULL) {            // オープンに失敗した場合
    fprintf(stderr, "エラー: ファイルがオープンできません: %s\n", argv[1]);
    exit(1);                          // 異常終了
  }

標準エラー出力が必要な理由

ところで、エラーメッセージはどうして標準エラー出力に出すべきなのでしょうか。 なぜ標準出力に出してはいけないのでしょうか。その理由を2つあげておきます。

  1. 標準出力に出すと、エラーメッセージと通常の出力が混ざってしまい、 何かと不都合です。 特に、リダイレクションやパイプが使われた場合に問題となります。 prog > file1 のようにリダイレクションが使われた時、 prog が標準出力にエラーメッセージを出していると、 通常の出力といっしょにエラーメッセージまで file1 に 流し込まれてしまいます。 その結果、エラーメッセージは端末画面に現れなくなり、 ユーザがエラーを見逃す危険があります。
  2. 標準エラー出力は、通常バッファリングされません。 エラーメッセージはただちに表示されたほうが良いので、 バッファリングされない標準エラー出力に出すほうが好都合です。

実を言うと、標準エラー出力もリダイレクトすることができます。 コマンドラインの世話をしているシェルがいわゆる Bourne(ボーン) シェル系のシェル であれば、prog 2> file と書くことで標準エラー出力だけをリダイレクトできます。 多くの Linux ディストリビューションで標準のシェルとなっている bash はこの系統のシェルです。 エラーメッセージだけをファイルに保存したい場合などに便利です。 残念ながら、10号館の標準設定でコマンドラインの世話をしてくれている tcsh の場合、標準エラー出力だけをリダイレクトすることはできません。 標準出力と標準エラー出力の両方を合わせてリダイレクトすることなら、 prog >& file1 のように書くことで可能ですが、 普通の出力とエラーメッセージが混ざってしまいます。 ちょっと不便ですね。

課題

再帰呼出しの章で例としてあげた階乗の計算をするプログラム を次のように改造しなさい:

  1. scanf を用いて標準入力からデータを読んでいる箇所は、 fgets と sscanf を用いる形に修正する。
  2. 負の数に対する階乗は定義されていないので、入力が負の場合には、 標準エラー出力にエラーメッセージを出して、異常終了するようにする。 そのときの終了ステータスは 1 とする。 エラーメッセージは、"エラー: 負の数の階乗は定義されません。\n" としておけばよいだろう。

コンパイル・実行の例:

% cc -o fact2 fact2.c
% ./fact2
5               ← 正常な値の入力
5の階乗は120.
% ./fact2
-1              ← 負数を入力
エラー: 負の数の階乗は定義されません。
% echo $status
1               ← 終了ステータス 1 で異常終了
%

プログラムのファイル名は fact2.c とせよ。