C言語編 第6章 標準入力①

先頭へ戻る

この章の概要

この章の概要です。

標準入力

これまで、printf関数や puts関数を使った、画面への「出力」を扱ってきました。 今回は、出力の反対である「入力」を扱います。

標準入力という言葉があります。 これは、標準的な(通常の方法を使った)入力の情報がどこからやってくるかを定義したものです。
具体的なことは、環境や設定によって変わりますが、パソコンの場合、通常はキーボードが割り当てられているはずです。 この場合、キーボードが標準的な入力方法であり、プログラムはキーボードから入力された情報を、 標準入力を通して使用することができます。

標準入力と同様の考え方で、標準出力というものもあります。 こちらは、標準的な出力の情報が、どこへ送られていくのかということです。 やはり、環境や設定で異なりますが、パソコンの場合なら、画面上へ出力(表示)されるのが一般的です。

もう1つ、標準エラーというものも存在します。 これはプログラムの実行中に起こったエラーの内容の出力がどこへ送られるかということです。 標準出力と同様に、画面が対象になっている場合がほとんどです。 標準出力と分離していることにより、エラーの出力先だけを変更するようなことも可能になっています。

fgets関数

出力のための関数が複数用意されているのと同様、入力のための関数も複数あります。 最初に説明するのは、fgets関数です。

多くの入門記事では、scanf関数という別の関数を最初の題材としていますが、この関数は実用上は多くの厄介事を抱えています。ここでは、問題の少ない fgets関数を使うことから始めたいと思います(scanf関数については、第7章で触れます)

fgets関数は、文字列を入力するための関数であり、puts関数と対照的な存在です。 名前の頭に付いている「f」は「file」を表しており、本来的には、ファイルの中身を読み取って、 その内容を入力データとするものですが、標準入力からの入力も可能です。

実のところ、標準入力がキーボードでも、ファイルでも、同じように扱えるようになっています。 想像しづらいですが、キーボードもファイルの一種だと考えてしまって構わないのです。

入力に関わる関数の中では、比較的問題の少ない fgets関数ではありますが、説明しないといけないことは結構あります。 まずはできるだけ単純な例をお見せしましょう。

#include <stdio.h>

int main(void)
{
    char str[80];  /* 入力された文字列を格納する場所 */


    puts( "何か文字列を入力してください。" );
    fgets( str, 80, stdin );

    /* 入力された文字列をそのまま出力する */
    puts( str );

    return 0;
}

実行結果:

何か文字列を入力してください。
abcde
abcde

ソースコードの中身を見る前に、とりあえず実行して試してみて下さい。 最初に、「何か文字列を入力してください。」と表示され、その状態で止まっていると思います。 このとき、キーボードから文字を入力できるので、何か適当な文字列を入力して下さい。 Enterキーを押すと確定します。 すると、入力した文字列がそのまま画面にもう一度表示されます。

paiza.IO の場合

paiza.IO で試す場合は、あらかじめ入力内容を設定してから、実行を行って下さい。 入力は、画面下部の「入力」タブを選択すると現れる空白部分に行います。

paiza.IO で標準入力を行うには

日本語を使うと、正しく動作しない可能性があります。 詳しいことは、第46章第47章で説明しています。

では、ソースコードを見ていきましょう。 main関数の最初の部分で、変数を宣言しています。 char型なので、文字を扱う変数ですが、変数名の後ろに [80] が付いています。 これは、配列と呼ばれるもので、要は、同じ型の変数がたくさん集まった集合体を表現したものです。 「char str[80];」であれば、char型の変数が 80個集まっているということです。 そのため、80文字まで扱える変数として使えます。
配列は、char型以外で使うこともできますが、説明することが多いので、 しばらくは、文字列を扱うときにだけ、最小限の使い方で使うことにします。 第25章で改めて詳しく説明します。

続いて、puts関数を使って、"何か文字列を入力してください。" と出力しています。 このようなメッセージを出力することによって、実行時に、何をすればいいかをユーザーに伝えます。

そして、fgets関数を呼び出します。この関数には3つの情報を渡します。
まず、入力された文字列を受け取る変数を指定します。 ある程度の長さの文字列の入力に耐えられるように、80文字分の配列を用意した訳です。
2つ目に、最大で何文字まで受け取るのかを指定します。 ここに指定する値は、先ほどの char型配列の大きさと一致させます。 そうしないと、非常に長い文字列が入力されると受け取りきれないため、プログラムがうまく動作しません。
3つ目に、どこから入力を受け取るかを指定します。 標準入力からの入力の場合は、必ず「stdin」と書きます。 stdin は「Standard Input」すなわち「標準入力」の略です。

fgets関数が実行されると、標準入力からの情報を待つ状態になります。 標準入力がキーボードであれば、ユーザーがキーボードから何か入力して、確定させるまで待機することになります。 入力内容が確定すると、その内容が fgets関数に渡した char型配列に格納されます。

最後に、char型配列に格納された文字列を、そのまま puts関数に出力させています。 このように、puts関数に char型配列を渡せば、その内容を出力できます。
ちなみに、printf関数を使う場合は、「printf( "%s\n", str );」のようにします。 "%s" は、文字列を出力することを指定する変換指定子です。 "\n" が不要なら、「printf( str );」のようにしても出力することができます。

ところで、先ほどのプログラムの実行結果をよく見ると、最後に1行、何もない行があることが分かります。 実は、fgets関数は、最後に押した Enterキーによる改行まで含めて受け取っています。 ここに更に、puts関数が自動的に行う改行が加わるため、その結果、空の行が余分に出来てしまいます。

これが問題であれば、余分な改行文字を後から取り除くようなプログラムを書くか、 puts関数の代わりに、printf関数を "\n" を入れずに使うことで、追加の改行をしないようにすることが考えられます。 今のところは、大した問題ではないので無視して進めることにします。

ヌル文字

先程のプログラムをもう少し掘り下げていきます。

サンプルプログラムでは、80文字まで扱える char型配列を使いましたが、 では 81文字以上入力されてしまったらどうなるでしょう? 81文字も入力するのは大変なので、文字数を減らして実験してみましょう。

#include <stdio.h>

int main(void)
{
    char str[5];  /* 入力された文字列を格納する場所 */


    puts( "何か文字列を入力してください。" );
    fgets( str, 5, stdin );

    /* 入力された文字列をそのまま出力する */
    puts( str );

    return 0;
}

実行結果:

何か文字列を入力してください。
abcdefg
abcd

char型配列の数値を 5 に変更しているので、5文字が受け取れる限界の文字数です。 この状態で実行して、「abcdefg」という 7文字を入力してみると、「abcd」とだけ出力されます。 4文字しか出力されていませんし、改行も行われていないようです。

7文字の入力に対して、4文字しか出力されなかったのは、C言語の文字列の表現方法に理由があります。 C言語の文字列は、末尾に見えない文字が隠されています。 「見えない」というのは、画面上には見えないという意味で、確かに存在はしている文字です。
この見えない文字は、ヌル文字と呼ばれていて、 ソースコード上に「見えるように」記述するときは、'\0'と書きます。 '\0' と書くと2文字あるように思えますが、これで1文字です。

ヌル文字は、文字列の末尾の位置を表すという目的があります。 ヌル文字がないと、文字列がどこまで続くのか分からず、謎めいたエラーの原因になります。 とりあえず、現状ではそこまで意識する必要はありませんが、C言語の文字列を理解するためには、 ヌル文字の存在を理解することは非常に重要です。 文字列については、第25章で改めて取り上げます。

fgets関数の2つ目の情報は、「最大で何文字まで受け取るのか」でしたが、 この数値には、ヌル文字の分も含まれている訳です。 ですから、5 を指定した場合は、ヌル文字を含めて 5文字ということです。

先程のサンプルプログラムの出力結果は、"abcd" の 4文字に見えますが、 本当は「abcd\0」という 5文字がきちんと存在しています。 ただ、あるはずの改行文字がなくなってしまっています。 本当なら「abcd\n\0」となることが理想なのですが、これだと6文字になってしまい入り切らないため、 \n が追い出されて、\0 が入っている状態です。

バッファオーバーフロー

さて今度は、5文字分の char型配列に対して、10文字ぐらいの長さの入力を与えたらどうなるのか、という話です。

#include <stdio.h>

int main(void)
{
    char str[5];  /* 入力された文字列を格納する場所 */


    puts( "何か文字列を入力してください。" );
    fgets( str, 10, stdin );  /* str は 5文字まで入らないのに 10 を指定した! */

    /* 入力された文字列をそのまま出力する */
    puts( str );

    return 0;
}

実行結果:

何か文字列を入力してください。
abcdefg
abcdefg
(この後エラーになる)

実行結果にあるように、画面上には 7文字分の文字列が表示されていますが、 直後にエラーメッセージが出て止まってしまいます。 正しく動作していないようです(実行環境によっては、結果が異なる可能性があります)。

5文字分の char型配列に、5文字までしか文字を格納しないように制御するのは、fgets関数の役目ですが、 限界の文字数を教えるのは、プログラマの役目です。 つまり、fgets関数を使うときに、2つ目の情報として、5 を渡してやらねばなりません。 この情報を渡すことによって、「最大でも 5文字までしか入力を受け取らないでほしい」と指示している訳です。

このサンプルプログラムのように、fgets関数に間違って 10 という情報を渡してしまったら、 受け取り側の char型配列の方が小さいため、文字列が溢れ出してしまいます。 これは、バッファオーバーフローと呼ばれる危険な現象です。 実行したときにエラーが出てしまうのは、これが原因です。

fgets関数を使ったときに起こり得るバッファオーバーフローに備えるには、 char型配列の大きさと、fgets関数に渡す2つ目の値は正確に一致させておくことです。 より間違いが起きにくくするために、次のように記述すると良いでしょう。

#include <stdio.h>

int main(void)
{
    char str[5];  /* 入力された文字列を格納する場所 */


    puts( "何か文字列を入力してください。" );
    fgets( str, sizeof(str), stdin );

    /* 入力された文字列をそのまま出力する */
    puts( str );

    return 0;
}

実行結果:

何か文字列を入力してください。
abcdefg
abcd

fgets関数の2つ目の情報を、「sizeof(str)」に変更しました。
sizeof は、(そうは見えませんが)演算子の一種です。 ( ) の内側に指定した変数の大きさに置き換わります。 str が 5文字分の char型配列であれば、sizeof(str) は 5 になるということです。 sizeof については、第20章で改めて詳しく取り上げます。

sizeof の ( ) は必須ではなく「sizeof str」と書いても構いませんが、一般に ( ) を付けることが多いようです。 ( ) があると関数みたいですが、あくまでも演算子です。

sizeof を使って書くことで、後から str の大きさを変更したとしても、自動的に fgets関数に渡す情報も更新されますから、 両者を一致させ忘れる心配がなくなります。

プログラミングの一般論として、「どこかを変更したら、ほかのどこかも変更しなければならないことがある」 という点は強く意識して下さい。 方向性として、連動しているコードは自動的に書き換わるように仕向けるべきです。 そうすれば、変更し忘れる可能性が大幅に減ります。 配列の大きさを、sizeof を使って指定することは、まさにこの発想から来るもので、有効な手法です。

注意を促すメッセージ

バッファオーバーフローを完全に防ぐことは、かなり骨の折れる作業です。 だったら、次のように注意を促すメッセージを出したらどうでしょう?

#include <stdio.h>

int main(void)
{
    char str[80];  /* 入力された文字列を格納する場所 */


    puts( "何か文字列を 80文字未満で入力してください。" );
    fgets( str, sizeof(str), stdin );

    /* 入力された文字列をそのまま出力する */
    puts( str );

    return 0;
}

実行結果:

何か文字列を 80文字未満で入力してください。
abcde
abcde

このようなメッセージを出しても駄目です。

こういう注意書きが守られる保証はありませんし(ましてや読んでくれる保証すらありません)、 プログラムの内容を知らない人は、「駄目なら駄目で、また何かエラーメッセージを出してくれるだろう」と思うかも知れません。
それに、入力する文字数をいちいち事前に数える人がどれほどいるでしょうか? 「入力しようとしているこの文字列だけど…80文字未満かどうか微妙だなあ。まあいいや試しに入力してみよう」と考えるのが普通ではないでしょうか?

基本的に、ユーザの良心に頼ったり、面倒事をユーザに押し付けたりするようなタイプの解決方法は適切とは言えません。

残された文字はどうなる

この辺で終わりにしたいところですが、まだ問題は続きます。 最大5文字まで受け取るように指示を与え、実際には 7文字入力されたとき、 結局のところ 4文字目までしか受け取っていないため、残りの 3文字が消えたように見えます。

この 3文字は、実は消えて無くなった訳ではなく、まだ存在し続けています。 詳しい技術解説をする段階ではないので、確認だけしておきます。

#include <stdio.h>

int main(void)
{
    char str[5];  /* 入力された文字列を格納する場所 */


    puts( "何か文字列を入力してください。" );
    fgets( str, sizeof(str), stdin );

    /* 入力された文字列をそのまま出力する */
    puts( str );

    /* もう1回繰り返してみる */
    fgets( str, sizeof(str), stdin );
    puts( str );

    return 0;
}

実行結果:

何か文字列を入力してください。
abcdefg
abcd
efg

fgets関数と puts関数をもう1回ずつ使ってみると、残された 3文字 "efg" が出力されました。 試してみると分かりますが、2回目の fgets関数のときには、ユーザーの入力を待機することすらありません。 これは、既に1回目の fgets関数で入力された "efg" が、どこかに取り残されているからです。 以前に受け取ったものが残っているので、それをまず出力したということです。

正確で確実な対処は難しいので、ここでは触れませんが、取り残されている前の入力情報というのは、 実際のアプリケーションではかなり厄介な問題です。 とりあえずは、要求以上の長さの文字列を入力しないように注意しておくことにしましょう。

gets関数

fgets関数と違い、完全に標準入力だけを対象とした gets関数という関数が存在します。

gets関数も、文字列を受け取る関数ですが、改行文字を受け取らないという違いがあります。 しかし、そんなことよりも遥かに重大な問題として、gets関数は受け取る文字数を指定できません。 そのため、バッファオーバーフローの問題に対して、gets関数は完全に無防備ですから、この関数は絶対に使ってはいけません。

そんなに危険なら、なぜこんな関数が用意されているのかと思うでしょう。 この関数が作られた頃には、危険性はあまり認識されておらず、後から重大な問題だと分かったのでしょう。 C言語のプログラムは、世界中に大量に存在しており、今さら gets関数を根絶するのも難しいところです。 突然、この関数を消し去ってしまったら、過去のプログラムはコンパイルできなくなってしまうかも知れません。
また、gets関数を使っていた箇所を、fgets関数に置き換える作業も簡単ではありません。 改行文字を受け取る・受け取らないの違い1つ取ってみても、まったく同じ動作になるように書き変えるのは難しいものです。

C11 (gets関数の削除)

gets関数の使用は危険なので、C11規格で削除されました。代わりに、C11 でオプション機能として追加された gets_s関数や、fgets関数を使用します。

clang 5.0.0 には、まだ残されています。


練習問題

問題① fgets関数で受け取った文字列を、標準出力へ2回出力するプログラムを作って下さい。

問題② fgets関数を2回呼び出して適当な文字列を2つ受け取り、それらをつなげて標準出力に出力するプログラムを作って下さい。

問題③ "x y z" という文字列には、何文字含まれていますか?(x と y の間と、y と z の間に半角の空白文字があります)


解答ページはこちら

参考リンク

更新履歴

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/1/31 タイトルを変更(キーボードから入力する -> 標準入力)
全面的に文章を見直し、修正を行った。 「バッファオーバーフロー」の項の内容の一部を、「ヌル文字」の項に分離した。
問題③が gets関数を試させるものになっていたが、本編では解説を省いているし、すでに削除されてしまっているコンパイラもあるので、 問題自体を変更した。

'2018/1/5 Xcode 8.3.3 を clang 5.0.0 に置き換え。

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

▼更に古い更新履歴を展開


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS

前の章へ(第5章 コメントの書き方)

次の章へ(第7章 標準入力②)

C言語編のトップページへ

Programming Place Plus のトップページへ