このページでは「タイムアウト付きの scanf」を実現する方法の解説と、これを利用した時間制限ありの4択クイズプログラムの紹介をしていきたいと思います!

タイムアウト付きの scanf か
考えたことないけどなんか簡単にできそうな気がする
文字だけ見ると簡単に出来そうだけど、
結構いろんな知識が必要だよ
試しに自分で作ってみると結構苦労すると思うよ!


確かに試してみたけどダメだった…
そもそも scanf にタイムアウト設定できないし無限待ちになっちゃう…
「タイムアウト付きの scanf」と聞くとなんだか簡単そうに聞こえるかもしれませんが、実は結構いろんな知識が必要で難易度は高めです。
ただ理屈さえわかればプログラミング自体は簡単にできると思います!
私は MacOSX で動作確認しましたが、他の OS 環境でも同様の方法で実現できるのではないかと思います。ただ他の OS での動作確認は出来ていないので上手く動作しないなどあればコメントや Twitter で質問していただければと思います。
タイムアウト付き scanf の実現方法
では早速、タイムアウト付き scanf の実現方法について解説していきたいと思います。
結論を言うと、scanf 関数のみでタイムアウトを実現することは不可能だと思います。なので、今回は select 関数と併用することで実現します。
scanf 関数の動作
scanf 関数は文字列入力を受け付け、入力された文字列を引数のアドレスに格納する関数です。
C言語を習いだして割とすぐに使い出す関数なので、scanf 関数に馴染み深い人も多いと思います。
scanf 関数の動作をもうちょっと詳細に記述すると下記のようになります。
- 標準入力が読み込み可能状態になるまで待つ
- 読み込み可能状態になったら標準入力からデータを読み込む
標準入力は基本的にターミナルやコンソールと捉えて良いです。
「標準入力が読み込み可能状態になる」のはこの標準入力に「文字列+エンターキー」が入力されたタイミングです。
つまり、scanf 関数実行後に標準入力に「文字列+エンターキー」が入力されたタイミングでこの入力されたデータが読み込まれ、引数で指定したアドレスにそのデータが格納されます。
ポイントは scanf 関数が「いつまで読み込み可能状態になるまで待つ」のかという点です。
ご存知の通り、scanf 関数はこの待ちを無限に行います。タイムアウトは発生しません。さらに、scanf 関数自体は、このタイムアウト時間設定するようなことも出来ません。
なので、scanf 関数のみでタイムアウトを発生させるようなことは出来ないのです。
そのため、今回は scanf 関数では「標準入力が読み込み可能状態になるまで待つ」動作を “行わせないようにする” アプローチでタイムアウト付き scanf 関数を実現していきたいと思います。
スポンサーリンク
select 関数と併用して実現
もうちょっと正確に言うと、select 関数で「標準入力が読み込み可能状態になるまで待つ」ようにし、読み込み可能状態になってから scanf 関数を実行するようにします。
ポイントは select 関数ではタイムアウトが設定できると言う点です。
select 関数でタイムアウトが設定できるため、「標準入力が読み込み可能状態になるまで待つ」時間が長い時にタイムアウトで関数を終了させることができます。
そして、タイムアウトが発生しなかった時だけ scanf 関数を実行すれば、タイムアウト付き scanf 関数を実現することができます。

ちょっと待って!
select 関数ってソケット通信用の関数じゃなかったっけ?
一番利用されるのはソケット通信時だと思うけど、別にそれ専用ってわけじゃないよ
ソケットだけじゃなくてファイルディスクリプタ全般に使用できる関数だよ!

select 関数については下記で解説していますので、使い方やどのような関数であるかはこちらを参照していただければと思います。
要は、ファイルディスクリプタが読み込み可能状態・書き込み可能状態になったかどうかを監視する関数で、これらの状態になるまで待ち合わせを行うことができる関数です。
さらに、この待ち合わせのタイムアウト時間も設定することができます。
上記ページではこの select 関数を利用して「タイムアウト付きデータ受信」を実現する方法を解説しています。
タイムアウト付きデータ受信時にはファイルディスクリプタとしてソケットを指定していました。
実は、ソケットと同様に標準入力にもファイルディスクリプタが割り当てられています。
ですので、ソケット同様に select 関数で「標準入力が読み込み可能状態になったかどうか」を監視することができます。
ここまでをまとめると、以下のように処理を実行することにより「タイムアウト付き scanf」を実現することができます。
select関数を実行する- 標準入力が読み込み可能状態になるまで待つ
select関数でタイムアウトが発生していなければscanf関数を実行する- 標準入力が読み込み可能状態になるまで待つ
- 読み込み可能状態になったら標準入力からデータを読み込む
select 関数でタイムアウトが発生した場合は、文字列入力のタイムアウトが発生したと言うことで scanf 関数を実行せずに終了します。
またタイムアウトが発生しなかった場合は、scanf 関数実行時はすでに「標準入力が読み込み可能状態」になっています。
ですので scanf 関数での「標準入力が読み込み可能状態になるまで待つ」の無限待ちを防ぐことができます。
タイムアウト付き scanf のプログラム
実際にタイムアウト付き scanf を実現したプログラムのソースコード例は下記のようになります。
#include <stdio.h>
#include <sys/select.h>
int main(void) {
struct timeval tv;
fd_set readfds;
int ret_select;
int fd = 0; /* stdinのファイルディスクリプタは0 */
char buf[256];
while (1) {
/* タイムアウト時間を設定 */
tv.tv_sec = 2;
tv.tv_usec = 0;
/* 読み込みFD集合を空にする */
FD_ZERO(&readfds);
/* 読み込みFD集合にstdin(0)を追加 */
FD_SET(fd, &readfds);
printf("Input string:");
/* ↑の表示を即座に行うためにfflushする */
fflush(stdout);
/* select関数で標準入力が読み込み可能になるまで待ち */
ret_select = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret_select == 0) {
/* タイムアウトが発生 */
printf("TIME OUT !!!\n");
/* 再度入力待ちに戻る */
continue;
} if (ret_select == -1) {
/* エラー発生時は終了 */
return -1;
}
/* 実際に文字列の読み込みを行う */
scanf("%s", buf);
/* 入力された文字列を表示 */
printf("Your input is [%s]\n", buf);
}
return 0;
}
while 文の中で標準入力が読み込み可能になるまで select で待ち、読み込み可能になったら scanf で文字列を読み込んでそれを表示するものになります。
select でタイムアウトになった場合は、scanf は行わずにループの先頭に戻って再度 select での待ちを行っています。
基本的にやってることはこれだけなのですが、1つだけポイントがあって、それは標準入力に対応するファイルディスクリプタが 0 である点です。
この 0 をファイルディスクリプタとして select 関数の引数を設定しています。
その他の select 関数の使い方などは下記ページを参考にしていただければと思います。
また、fflush を実行しているのは、その直前の printf の表示を即座に行うためです。
通常 scanf を実行すれば、その時点で標準出力がフラッシュされるので printf の表示がそのタイミングで行われます。
ですが、select で文字列入力待ちを行うようにすると、select 待っている間フラッシュされないので(少なくとも私の環境ではフラッシュされなかった)、強制的にフラッシュを行うように fflush を実行しています。
この fflush の解説は下記ページで行っていますので、fflush に興味のある方はこのページの後にでも読んでみてください。
時間制限ありの4択クイズ
最後に、この「タイムアウト付き scanf」を利用して作成した「時間制限ありの4択クイズ」プログラムを紹介しておこうと思います。
通常の4択クイズと作り方はほぼ一緒ですが、「タイムアウト付き scanf」を利用して問題への回答が時間切れの場合は不正解とみなすようにしているところがポイントになります。
下記がその「時間制限ありの4択クイズ」プログラムのソースコードになります。
#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>
typedef struct _QUIZ {
char question[256]; /* 問題文 */
char selection[4][256]; /* 選択肢(4つ) */
int answer; /* 解答番号 */
} QUIZ;
int main(void) {
/* クイズの情報を格納 */
QUIZ quizs[5] = {
{
"日本一狭い都道府県は?",
{
"大阪府", "東京都", "香川県", "沖縄県"},
3
},
{
"日本一広い県は?",
{
"長野県", "福島県", "新潟県", "岩手県"},
4
},
{
"日本一人口が少ない県は?",
{
"徳島県", "鳥取県", "島根県", "高知県"},
2
},
{
"日本一人口密度が低い都道府県は?",
{
"北海道", "岩手県", "島根県", "鳥取県"},
1
},
{
"日本で一番多い苗字は?",
{
"佐藤", "鈴木", "高橋", "田中"},
1
}
};
struct timeval tv;
fd_set readfds;
int ret_select;
int fd = 0; /* stdinのファイルディスクリプタは0 */
char buf[256];
int i, j;
int point = 0;
for (i = 0; i < 5; i++) {
/* 問題文と選択肢を表示 */
printf("%s\n", quizs[i].question);
for (j = 0; j < 4; j++) {
printf("%d:%s ", j + 1, quizs[i].selection[j]);
}
printf("\n");
/* タイムアウト時間を設定 */
tv.tv_sec = 2;
tv.tv_usec = 0;
/* 読み込みFD集合を空にする */
FD_ZERO(&readfds);
/* 読み込みFD集合にstdin(0)を追加 */
FD_SET(fd, &readfds);
printf("答えは? : ");fflush(stdout);
/* select関数で標準入力が読み込み可能になるまで待ち */
ret_select = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret_select == 0) {
/* タイムアウトが発生 */
printf("時間切れです!!!!\n\n");
/* 再度入力待ちに戻る */
continue;
} if (ret_select == -1) {
/* エラー発生時は終了 */
return -1;
}
/* 実際に文字列の読み込みを行う */
scanf("%s", buf);
/* 正解かどうかを判断して表示 */
if (atoi(buf) == quizs[i].answer) {
printf("正解です!!!\n\n");
point++;
} else {
printf("不正解です...\n\n");
}
}
printf("%d問正解です!!!\n", point);
return 0;
}
タイムアウト付き scanf のプログラムとの大きな違いは問題文を表示するようにしたのと正解不正解の判断をするようにしたことくらいなので、説明は省略させていただきます。
実行すると下のアニメーションのように、問題表示後に2秒間回答がなければ回答受付けを終了して次の問題を表示するようになっています。

ちなみに解答は私が今日(2021/3/23)ググって調べたものですので、今後変わる可能性があります。
スポンサーリンク
まとめ
このページでは「タイムアウト付き scanf の実現方法」の解説を行いました!
scanf を実行する前に select 関数で「標準入力が読み込み可能状態」になるのを待つようにすることで、タイムアウト付き scanf を実現することができます。
これは、select 関数は scanf 関数と違ってタイムアウトを設定することができる点&標準入力にもファイルディスクリプタが割り当てられていて select 関数を適用することができる点を利用した実現方法になります。
今回は select と併用することで実現しましたが、他の方法でもタイムアウト付き scanf は実現可能だと思います。
ぜひ皆さんもいろんな方法を駆使してタイムアウト付き scanf を作成してみてください!

