[迷信] gets は単純に fgets に置き換えられる

何かと悪名高い gets 関数ですが、確かにバッファオーバーランを防げないなど、安全性に欠けるところがあります。そこで、これを何とかするために、gets を記述していた箇所を単純に fgets に置き換えればそれで問題が解決したと考えている方も少なくないようです。

ところが、実際にはそんな簡単なものではありません。getsfgets では仕様が異なりますし、gets のときは考えなくてもよかったことが、fgets では発生します。まずは具体例から見ていきましょう。

char s[5];
gets(s);

上のようなコードを書いた場合、5 文字以上を入力しようとするとたちまちバッファオーバーランが発生してしまいます。そこで、

char s[5];
fgets(s, 5, stdin);

とすることで解決したと安心してしまうわけですが、この場合に 5 文字以上を入力した場合にどうなるかを考えてみてください。例えば、

12345

を入力したとすると、配列 s には "1234" が格納されます。s[4] には '\0' が格納されます。そして、ストリームには格納し切れなかった'5' と改行文字が残ります。当然、この後次の行を入力しようとすると、"5\n" が読み込まれることになるわけです。これは gets では考慮する必要がなかったことです。

また、4 文字ちょうどを入力した場合、例えば、

1234

を入力したとすると、やはり配列 s には "1234" が格納され、ストリームに改行文字が残ります。当然、次に getchar で 1 文字読み込もうとすると改行文字が出てきますし、fgets で 1 行読み込もうとすると、改行文字だけが読み込まれることになります。gets ではこのようなことは起こりませんでした。

さらに、3 文字以下を入力した場合、例えば、

123

を入力したとすると、配列 s には "123\n" が格納されます。gets のときには "123" が格納されたので、最後の改行文字が余分に格納されていることになります。入力した文字列が例えばファイル名だった場合、そのまま fopen 関数に渡そうとすると、(改行文字が含まれているので)当然失敗します。

多くの掲示板やメーリングリスト、ウェブサイト、ときには書籍においても、gets は危険なので fgets に置き換えるべきであるという指摘をよく見かけます。しかし、こうした仕様の違いを示さず、あるいは fgets を用いた場合の具体的なコードを示さず、単純に置き換えれば済むかのような発言は、特に C 言語の経験が浅いプログラマをさらに困惑させることになります。

学校の課題などで、文字列の入力そのものが本題ではないのであれば、とりあえず gets で手軽に入力処理を記述しておき、本題に注力するのは悪くありません。また、学校の課題ではなくても、コード片の実験であったり、その場限りの使い捨てプログラムであれば、理解した上で gets を使ってもやはり問題はありません。

最後に、fgets を用いて本格的に gets を置き換える場合の例を示すことにしましょう。

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 でバッファを拡張しながら残りの文字を読み込む必要があります。

参考情報

このエントリーを含むはてなブックマーク