何かと悪名高いgets関数ですが、確かにバッファオーバーランを防げないなど、安全性に欠けるところがあります。そこで、これを何とかするために、gets関数を記述していた箇所を単純にfgets関数に置き換えればそれで問題が解決したと考えている方も少なくないようです。
ところが、実際にはそんな簡単なものではありません。gets関数とfgets関数では仕様が異なりますし、gets関数のときは考えなくてもよかったことが、fgets関数では発生します。まずは具体例から見ていきましょう。
0 1 2 3 |
char s[5]; gets(s); |
上のようなコードを書いた場合、5文字以上を入力しようとするとたちまちバッファオーバーランが発生してしまいます。そこで、
0 1 2 3 |
char s[5]; fgets(s, 5, stdin); |
とすることで解決したと安心してしまうわけですが、この場合に5文字以上を入力した場合にどうなるかを考えてみてください。例えば、
0 1 2 |
12345 |
を入力したとすると、配列sには”1234″が格納されます。s[4]には’\0’が格納されます。そして、ストリームには格納し切れなかった’5’と改行文字が残ります。当然、この後次の行を入力しようとすると、”5\n”が読み込まれることになるわけです。これはgets関数では考慮する必要がなかったことです。
また、4文字ちょうどを入力した場合、例えば、
0 1 2 |
1234 |
を入力したとすると、やはり配列sには”1234″が格納され、ストリームに改行文字が残ります。当然、次にgetchar関数で1文字読み込もうとすると改行文字が出てきますし、fgets関数で1行読み込もうとすると、改行文字だけが読み込まれることになります。gets関数ではこのようなことは起こりませんでした。
さらに、3文字以下を入力した場合、例えば、
0 1 2 |
123 |
を入力したとすると、配列sには”123\n”が格納されます。gets関数のときには”123″が格納されたので、最後の改行文字が余分に格納されていることになります。入力した文字列が例えばファイル名だった場合、そのままfopen関数に渡そうとすると、(改行文字が含まれているので)当然失敗します。
多くの掲示板やメーリングリスト、ウェブサイト、ときには書籍においても、gets関数は危険なのでfgets関数に置き換えるべきであるという指摘をよく見かけます。しかし、こうした仕様の違いを示さず、あるいはfgets関数を用いた場合の具体的なコードを示さず、単純に置き換えれば済むかのような発言は、特にC言語の経験が浅いプログラマをさらに困惑させることになります。
学校の課題などで、文字列の入力そのものが本題ではないのであれば、とりあえずgets関数で手軽に入力処理を記述しておき、本題に注力するのは悪くありません。また、学校の課題ではなくても、コード片の実験であったり、その場限りの使い捨てプログラムであれば、理解した上でgets関数を使ってもやはり問題はありません。
最後に、fgets関数を用いて本格的にgets関数を置き換える場合の例を示すことにしましょう。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
char s[5]; size_t n; if (fgets(s, 5, stdin) == NULL) /* エラー処理 */; n = strlen(s); if (s[n - 1] == '\n') { s[n - 1] = '\0'; } else { int c; do { c = getchar(); if (c == EOF) { if (feof(stdin)) break; else /* エラー処理 */; } } while (c != '\n'); } |
決してお手軽とはいえませんね。そして、上のコードではバッファに格納できなかった文字は捨てていますが、捨ててはいけない場合には、realloc関数でバッファを拡張しながら残りの文字を読み込む必要があります。