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

stdin と stdout

ストリームを用いたファイル入出力について色々と学んできました。 これで scanf を使った標準入力の読み込みや、printf を使った標準出力への書き込みと、 fgets を使ったファイルの読み込みや、fprintf を使ったファイルへの書き込みの二種類の入出力方法を学んだように思えます。

が、実際には両者は同じものです。 標準入力や標準出力もストリームの一種です。 皆さんは、気づかないうちにすでにストリーム入出力を使っていたのです。 単に必要なはずのオープン・クローズ処理や、FILE ポインタの記述を省略させてもらっていただけです。

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

プログラマが標準入力・出力のオープン処理や FILE ポインタの記述を省略できるのは、OS がプログラムの実行が始まったときには既に(自動的に)オープンされてしまっていたからです。

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

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

stderr

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

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

一般的には fprintf 関数を以下のように用いて利用します。

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

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

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

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

エラーメッセージを標準エラー出力に(標準出力とは別に分けて)出すべき理由は幾つかあります。 主たる理由はリダイレクション対策です。

標準出力をリダイレクションしたりパイプにつないだりすると、端末で操作している人にエラーメッセージが見えません。 標準エラー出力はリダイレクションされないため、prog > file1 のようにリダイレクションされてもエラーメッセージが端末画面に表示され、ユーザがエラーを見つけることができます。

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

課題

再帰呼出しの章で例としてあげた階乗の計算をするプログラム を、入力が負の場合には標準エラー出力に何らかエラーメッセージを出して終了するように修正してください。(負の数に対する階乗は定義されていない)

コンパイル・実行の例:

% cc -o fact2 fact2.c
% ./fact2
5               ← 正常な値の入力
5の階乗は120.
% ./fact2
-1              ← 負数を入力
エラー: 負の数の階乗は定義されません。
%


補足:scanf 関数の危険性

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

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

このようにすべき理由はいくつかありますが、最大のものは scanf がバッファのあふれ(バッファオーバフロー) を考慮しないことです。 例えば、scanf("%s", buffer); を実行すると、文字列が読み込まれてバッファにつめ込まれますが、文字列の長さがバッファのサイズを越えていても scanf は全くお構いなしにバッファにつめようとします。 その結果、バッファの終端を越えてデータが書き込まれてしまい、プログラムの動作が狂う可能性があります。 ここ何年か、この種の欠陥をついてコンピュータを不正利用する攻撃が増えています。

クラスではファイル入出力を伴う fgets() を最初に教えるわけにいきませんから、危険ではあるけれども単純な sscanf() を使って教えました。 しかし問題点がわかった以上、実用のプログラムでは決して scanf を使わないようにして下さい。

つまり上の課題は scanf() を使っていますから、これは stdin を利用した fgets() と sscanf() に書き換えるべきですね。

バッファオーバフローの危険がある関数、記述は scanf だけではありません。 関心のある人は調べてみると良いでしょう。 セキュリティ問題の重要な入口となります。