getchar関数に見る入力処理

Cプログラミング講座



0.参考にした書籍内のページ

本講座は、「プログラミング言語C、第2版」のP.36に書かれている関数getlineのソースを参考にしています。

実行時の処理を頭で追う際に、forループとライブラリ関数getcharの組み合わせにより、文字列を格納している点の処理がうまく想像できなかったので、自身で検証プログラムを作るとともに、わかった点をまとめています。


1.getchar関数の使いかた

入力を受け取るライブラリ関数は数多くありますが、その中で標準入力(キーボード)から1文字を受け取るgetcharがあります。
ライブラリ関数getcharは、実行されるとキーボードからの入力を待ち、エンターキーが押されると入力待ちを終了、入力したうちの1文字目を返り値として返します。

たとえば

/* getchar.c                */
/* getcharを使い文字を格納  */
/* その後格納文字を表示する */
#include <stdio.h>

void main(void){
	int c;
	c = getchar();
	putchar(c);
}

のようなソースを打ち込み、コンパイル後実行してみると、さきほど述べた挙動が実際に理解できると思います。
aと打てばaが表示され、123と打ち込めば、1が表示されるわけです。

それでは、今度はこのようなコードを実行してみましょう。

/* getline_1.c                                     */
/* 1行格納し、その後表示                           */
/* 最大読み込み文字数は改行とヌル文字を除く999文字 */
#include <stdio.h>	//getchar用
#define MAXLINE 1000	//最大読み込み文字数

void getline(char line[], int maxline);

int main(void){
	char str[MAXLINE];	//文字列を格納する配列
	
	getline(str, MAXLINE);	//strに文字列を格納
	printf("格納した文字列:%s\n", str);	//表示
	
	return 0;
}

/*
 * 関数名:getline
 * 引数:文字列領域、文字列領域の長さ
 * 機能:getchar関数を用いて、標準入力から文字列を受け取る
 *      エンターキーを押す事により決定、入力文字が規定文字数以上だった場合は
 *      それまでを格納する
 * 返り値:なし
 */
void getline(char s[], int lim){
	int c, i;	//getcharの受け取り用変数c、ループ用変数i
	
	for (i = 0; i < lim - 1 && (c = getchar()) != '\n'; ++i)
		s[i] = c;
	
	s[i] = '\0';
}

今回のソースは、実行するとキーボードから文字列を受け取り、それをそのまま表示する関数です。
入力を求められたら、何文字か入力後エンターキーで決定することにより、その文字列が格納され、printfで表示されます。
こちらは、aと打ち込めばaが、123と打ち込めば123が表示されます。


2.自作関数getlineのはたらき

今回注目すべきは、自作関数getlineです。
関数getlineは、キーボードから1文字を受け取るライブラリ関数getcharを使い、引数として渡された配列sに文字列を格納します。

関数の動作としては、まず最初に変数宣言があり、そしてfor文の初期化処理、その後条件文をチェックします。
ここでgetchar関数が実行され、その値を変数cに代入し、それが改行でなければループ内の処理で文字列格納を実行、というように処理が進みます。
ところで、実際に実行してみた人は、getchar関数に入力を求められた際には、1文字だけでなく、何文字か1度に入力したと思います。
それでは、getchar関数は1文字しか入力を受け取れないのに、打ち込むのは1文字以上でもきちんと文字列が格納されているのはなぜでしょう?

例えば関数getline内の入力部分c = getchar()において、"abcde"と打ち込み、エンターキーを押したとします。ちなみにこのときcに格納される値は1文字'a'だけです。

この'a'を文字列に格納した後、さきほどの入力は無効となり、再び条件文で入力を求められるように思えますが、そのままプログラムは終了してしまい、"abcde"をsに格納して関数を終了、main関数のprintfで文字列の表示をしてしまいます。

なぜ文字列を格納できるのか? 残りの"bcde"はどこへ行ったのか?

疑問を解決するために、getchar関数がどのように文字列を受け取っているのか、コードの修正による実験で確かめていきましょう。


3.ライブラリ関数getcharの動作

getcharは、返り値はint型で、EOF(ファイル終端を示す特殊な値)を含む、標準入力から受け取った1文字のASCIIコードを返す標準ライブラリ関数です。
ですが、エンター(リターン)キーを押した時に入力を決定するので、何文字でも入力できてしまいます。
何文字も入力した場合、どのような振る舞いをするのか、それを調べてみましょう。

/* getline_2.c                            */
/* printfを追加して                       */
/* getline中のgetcharの動作を検証している */
#include <stdio.h>	//getchar用
#define MAXLINE 1000	//最大読み込み文字数

void getline_2(char s[], int lim);

int main(void){
	char str[MAXLINE];	//文字列を格納する配列
	
	getline_2(str, MAXLINE);	//strに文字列を格納
	printf("格納した文字列:%s\n", str);	//表示

	return 0;
}


/*
 * 関数名:getline_2
 * 引数:文字列領域、文字列領域の長さ
 * 機能:getchar関数を用いて、標準入力から文字列を受け取る
 *      エンターキーを押す事により決定、入力文字が規定文字数以上だった場合はそれまでを格納する
 *      printfによりプログラムの処理をわかりやすくしている
 * 返り値:なし
 */
void getline_2(char s[], int lim){
	int c, i;		//getcharからの受け取り用変数c、ループ用変数i
	for (i = 0; printf("%d回目の条件式参照\n", i + 1) && i < lim - 1 && (c = getchar()) != '\n'; ++i){
		s[i] = c;
		printf("%d番目の文字として%cを格納しました\n", i + 1, c);
		//格納の確認
	}
	
	s[i] = '\0';
}

プログラムや関数の動作の順序を確認する上で、プログラムの合間合間にprintfを使う、という手法があります。
プログラム中の変数の値を見たり、単純に文字を表示させるだけでも、その表示があった時点でprintfの前後を処理しているということがわかります。

これを用いてgetcharの振る舞いを確認するためのサンプルプログラムを用意しました。
for文の反復条件式であるgetchar関数がどのようなタイミングで実行されるかを確認します。
関数getcharを実行している条件文の前にprintfを追加し、いつ条件文を参照したかを、1文字を配列に格納した直後にもprintfを配置し、どのように格納がされていくかをそれぞれ表示させます。
for文の反復条件文にprintfを&&演算子で併記していますが、実はprintfはint型を返す関数で、出力した文字数を返します。
ゆえに文字が表示されるならば常に真なので、この条件式でprintfは全く無関係となり、元の条件式のみを判定するように動きます。


4.実行結果及びgetchar関数の実態

(わかりやすくするために、入力の際に表示される文字は太字と下線にしています、エンターキーも表示)
※文字列"abcde"を格納してみます

実行結果

$./getline_2
1回目の条件式参照
abcde[Enter]
1番目の文字としてaを格納しました
2回目の条件式参照
2番目の文字としてbを格納しました
3回目の条件式参照
3番目の文字としてcを格納しました
4回目の条件式参照
4番目の文字としてdを格納しました
5回目の条件式参照
5番目の文字としてeを格納しました
6回目の条件式参照
格納した文字列:abcde

実行結果ここまで

実行結果から、考察

"abcde"という文字列を打ち込み、エンターキーを押してみます。
すると、1回目の入力以降はfor文の条件式としてgetchar関数が処理されても、こちらに入力を求めるのではなく、自動で以前に入力した文字から入力を得て、処理を進めているように見えます。

そして実行結果の最後、6回目の条件式参照、においてループを抜けていますが、printfによる表示がなされている以上、c != '\n'が偽である必要があるので、つまりcには改行タグが入っているということです。
5回目の条件式参照で'e'を格納している事を考えれば、"abcde"の次に入力したものを変数cに格納していると考えられます。
"abcde"の次に入力したもの……それは決定として入力したエンターキーです。
決定の意であるエンターすらも改行コードとしてgetchar関数は受け取っていることもわかります。

考えられる事をまとめてみますと、

ということです、そろそろgetchar関数の特徴が掴めてきたのではないでしょうか?

今度は別の側面からもう少し、入力に関して掘り下げてみましょう。

/* getline_3.c                                        */
/* getcharで1文字取った後に                           */
/* 1行入力を受け付けるfgetsを標準入力に対し使ってみる */
#include <stdio.h>

int main(void){
	int c;	//getchar用
	char buf[100];	//fgets用
	c = getchar();	//文字列の先頭1文字を受け取る
	fgets(buf, 100, stdin);	//残りを格納
	 	
	printf("getchar:%c\nfgets:%s", c, buf);	//表示
	
	return 0;
}

実験として、getchar関数の後にfgets関数で標準入力から読み込むコードを作ってみます。
getchar関数では1文字しか格納しませんが、その後にfgets関数で文字列を読み込めば、考察どおりなら「1文字目以降の残り」を入力として得る事ができます。
実際に"abcde"と入力した時には、getchar関数に'a'のみが渡り、"bcde"がfgets関数によって格納されました。

getline_3.cに"abcde"を渡した際の実行結果

$./getline_3.c
abcde[Enter]
getchar:a
fgets:bcde

これにより、キーボードなどから入力をする際に、求められた以上の入力があった場合、それをどこかに保存しておき、次に入力要求があった時には、すぐに保存していた内容を渡すようになっていることが確かめられました。
またこの時順序は入力した通りに渡されます、このような方式を待ち行列と呼びます。

以上、getchar関数の少し特殊な使い方から、入力のストリーム的振る舞いまで考察することができました。


5.その他入力処理や、fgetsに関して

参考ページ:続・KITの私的ページ
今回、getline_3.cにてgetsではなくfgetsを使った理由や、そのほか入力関数に関する詳しい話が載せられています。
また、このサイトに記述があるように、fgets関数で文字列を受け取るときに、引数の規定文字数以上の入力があった場合も、それ以降の入力は保持され、次の入力時に流れてしまうそうです。
ループ文によるfgets関数の実行と、その条件部にstrchr関数による改行コードの検出を用いるなど、バッファの掃除を行う作業についても詳しく書かれています。

バッファに残る入力によるバグが起こったときは案外気付きにくそうなので、入力関数が受け取る際にあふれた入力の待ち行列化や、それを上手く用いたコーディング、様々な入力関数による振る舞いを理解して、バグに対処できるよう心がけましょう。


第2回講座へ
第3回講座へ

Cプログラミング講座 トップページへ


ページ作成者:elde
連絡先:s111058@wakayama-u.ac.jp