C言語編 第6章 キーボードから入力する

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

標準入力

これまで、printf関数や puts関数を使って、画面への「出力」を扱ってきました。 今回は、出力の反対である「入力」を扱います。 入力の方法には様々ありますが、ここで扱うのはキーボードからの入力です。

C言語では標準入力という言葉が登場することがあります。 C言語の全てのプログラムは、実行時に自動的に、標準入力が使える状態になります。 標準入力が、必ずしもキーボードであるとは限らないのですが、ほとんどの環境ではキーボードです。

標準入力と同様に、標準出力というものもあります。 これも、プログラムの実行時に自動的に使える状態にあり、ほとんどの環境では画面が対象になっています。

標準入力にせよ、標準出力にせよ、その対象を変更する手段が存在しますが、 とりあえずその辺りのことは考えず、キーボードと画面を使っていきましょう。

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

fgets関数

出力のための関数が複数用意されているのと同様、入力のための関数も複数あります。 最初に説明するのは fgets関数(⇒リファレンス)です。

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

fgets関数は、文字列を入力するための関数であり、puts関数と対照的です。 名前の頭の「f」は「file」を表しており、本来的にはファイルから入力を行うものですが、標準入力からの入力も可能です。

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

入力に関わる関数の中、比較的問題の少ない 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]; であれば、80文字まで扱える変数になります。 配列については、第25章で詳しく説明するとして、しばらくは文字列を扱うときにだけ使うことにします。

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

そして、fgets関数を呼び出します。この関数には3つの情報を渡します。 まず、入力された文字列を受け取る変数を指定します。
2つ目に、最大で何文字まで受け取るのかを指定します。ここに指定する値は、先ほどの char型配列の大きさと一致させます。
3つ目に、どこから入力を受け取るかを指定します。標準入力からの入力の場合は、必ず「stdin」と書きます。 stdin は「Standard Input」すなわち「標準入力」の略です。

fgets関数を呼び出すと、画面は停止した状態になり、ユーザーがキーボードから入力を行うのを待ちます。 Enterキーが押されると、入力された内容が渡され、fgets関数に渡した char型配列に格納されます。

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

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

バッファオーバーフロー

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

#include <stdio.h>

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


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

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

	return 0;
}

実行結果:

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

実行結果にあるように、「abcdefg」という 7文字を入力してみました。すると、「abcd」とだけ出力されます。

まず、5文字分の char型配列に、5文字までしか文字を格納しないように制御しているのは、fgets関数に 5 という情報を渡しているから出来ることです。 fgets関数に対して、「最大でも 5文字までしか入力を受け取らないでほしい」と指示している訳です。

もし、fgets関数に間違って 10 という情報を渡してしまったら、受け取り手である char型配列の方が小さいため、文字列が溢れ出してしまいます。 これはバッファオーバーフローと呼ばれる危険な現象です。 実際に試してみましょう。

#include <stdio.h>

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


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

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

	return 0;
}

実行結果:

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

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

このような、バッファオーバーフローを起こさないようにするには、 前に書いたように、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章で改めて詳しく取り上げます。
このように書くことで、後から、str の大きさを 10 に変更したとしても、自動的に fgets関数に渡す情報も更新されますから、 両者を一致させ忘れる心配がなくなります。

プログラム内に、同じ意味のコードを2回以上書くことは避けるのが基本です。 同じ意味のコードは1か所にまとめるようにして、そこだけを変更すれば、 関係のある全ての箇所が、自動的に変更される状態を作るべきです。 そうすることで、処理の不一致を避けることができます。

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

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

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

残された文字はどうなる

この辺で終わりにしたいところですが、まだ問題は続きます。 先ほどの例のように、最大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関数のときには、ユーザーの入力を待機することすらありません。 これは、既に前の fgets関数で入力された "efg" 及び、最後の Enterキーによる確定情報が存在しているからです。

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

注意を促すメッセージ

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

#include <stdio.h>

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


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

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

	return 0;
}

実行結果:

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

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

gets関数

fgets関数と違い、完全に標準入力だけを対象とした gets関数(⇒リファレンス)という関数が存在します。

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

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

C11 (gets関数の削除)

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

VisualC++ 2013 には gets関数は残されていますが、2015以降は削除されています。 clang 3.7 では残されています。


練習問題

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

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

問題B 適当な文字列を受け取って出力するプログラムを gets関数で作成し、gets関数が抱える問題点を考え、 実際に問題となることを確認してみて下さい。その問題が fgets関数で解決できるかも確認して下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/6/15 「paiza.IO の場合」の項を追加。

'2017/6/6 「C11 (gets関数の削除)」の項を追加。

'2015/8/19 文章の言い回しを一部修正。

'2013/4/27 gets関数が C11 で削除された旨を、コラムに追加。

'2010/2/27 main() を main(void) に修正。

'2009/3/7 「この章の概要」を追加。「gets関数」にコラムを追加。

'2008/12/26 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ