gets関数|危険性と代替関数【gets_s/fgets/scanfによる代替処理】

gets関数の概要について解説します。またgetsの問題点とその解決方法をいくつか解説します。後半ではfgets/scanf関数でgets関数の挙動を実現させる方法や、安全な代替処理を実現する方法、代替関数を自作する方法などを紹介します。

目次

gets関数

gets関数は標準入力の入力文字列を配列に書き込むための関数です。バッファオーバーランの危険性があるため非推奨の関数となっており、現在では廃止されています。

#include <stdio.h>
char *gets(char *s);
戻り値成功時:s,失敗時:空ポインタ(NULL)
第一引数読み取り文字列の書き込み先

getsは標準入力(stdin)から、改行文字またはファイル終端までの文字列を配列に対して書き込みます。最後に書き込まれた文字の直後にはナル文字(\0)が書き込まれます。書き込みが成功した場合には、引数に与えられた配列sを戻り値として返します。

配列に対して一文字も書き込みが行われなかった場合には、戻り値として空ポインタ(NULL)を返します。その際、配列の内容は変化しません。なお、読み込みが失敗した場合にも同様に空ポインタが返されますが、配列の内容は不定なものとなるため注意が必要です。

#include <stdio.h>

int main() {
  char s[3] = {'x', 'x', '\0'};
  char *p = gets(s); // 入力値: 'ab\n' | '\n' | ''
  p;                 // 代入値: 'ab\0' | '\0' | (NULL)
  s;                 // 格納値: 'ab\0' | '\0' | 'xx\0'
}

gets関数の危険性

gets関数は書き込み対象の配列サイズを意識せずに入力データを書き込むため、場合によっては配列のサイズを超えた余計な書き込みが行われてしまう危険性があります。誤って使わないように注意してください。

char s[5];
gets(s);        // 入力: "1234\n"
printf("%s",s); // 出力: "1234"
// バッファオーバーランの危険性あり
gets(s);        // 入力: "12345678\n"
printf("%s",s); // 出力: "12345678"(5文字以上書き込まれてる)

本来であれば末尾文字を含めて5文字までしか書き込みが行えないchar s[5]配列ですが、実際には"12345678"分の文字列が書き込まれてしまっていることがわかります。

余分に書き込まれた"678"は他のメモリー領域を上書きすることに繋がり、場合によっては重要なデータ領域を書き換えてしまったり、重大なセキュリティホールを生み出す原因にもなります。

gets関数はこれらの問題を回避することができないため、現在では廃止されており、代替関数の利用が求められています。

処理系によっては、gets関数利用時に警告が発生する場合があります。

char s[5];
gets(s); // 実行時警告:warning: this program uses gets(), which is unsafe.

gets関数はscanf関数の%s指定時と同様、ソフトウェアの脆弱性を生み出す危険な関数です。gets関数が利用されたプログラムはクラッカーにとっては格好のハッキング対象となるため、必ず利用を避ける必要があります。

代替関数

gets_s関数

gets関数の代わりとしては、より安全なgets_s関数が知られていますが、環境によっては利用できない関数であり、移植性に難があります。

// #include <stdio.h>
// char *gets_s(char *s, rsize_t n);

char s[5];
gets_s(s, 5); // 第二引数にバッファサイズを指定できる

なお、対応している環境であれば、gets_s関数は<stdio.h>ヘッダーに宣言されています。

fgets関数

その他の代替関数としてはfgets関数が有名ですが、gets関数と挙動が大きく異なるため、完全な代替としては利用できません。

入力結果に改行文字が含まれてしまう点や、入力ストリーム上にバッファが残ってしまうという特徴があります。

char s[5];

// 改行文字が含まれてしまう問題
fgets(s, 5, stdin); // 入力: "1234\n"
printf("%s",s);     // 出力: "1234\n"

// バッファクリアがされない問題
fgets(s, 5, stdin); // 入力: "12345\n"
printf("%s",s);     // 出力: "1234"
putchar(getchar()); // 出力: "5" (空読み必要)
putchar(getchar()); // 出力: "\n"(空読み必要)
getchar();          // 入力待ち状態
これらの問題を解決するためには自前で代替処理を記述する必要があります。次の節を参考にしてください。

代替処理

fgetsでgetsを置き換える

移植性を考えた場合、fgetsでの代用がベストな選択肢ですが、ただし上記の問題を回避するための工夫が必要となります。

具体的には、不要な文字の空読み処理に加えて、改行文字\nを終端文字\0に置き換える処理が必要となります。

char s[5];
fgets(s, 5, stdin); // 入力:"123456\n"

char *ln = strchr(s, '\n'); /* 改行文字を検索 */

if (ln != NULL) { /* 改行が読み取られていたかどうか */
   *ln = '\0';    /* 改行文字を終端文字に置き換える */
}
else {            /* 入力ストリーム上に文字が残ってる場合 */
   while (1) {    /* 改行文字が読み取られるまで空読みする */
      int c = getchar();
      if (c == '\n' || c == EOF) break;
   }
}

printf("%s", s);    // 出力:"1234"

これでgets関数とほぼ同等の挙動を実現することができます。

scanfでgetsを置き換える

こちらはscanf関数でgets関数を再現する方法です。

char s[5];
if (scanf("%4[^\n]%*[^\n]", s) < 1) *s = '\0';
getchar();

安全性は高いですが、少しトリッキーな方法を用いています。トリックの詳細については以下のページを参考にしてください。scanfの戻り値と代入抑止文字の説明が参考になるはずです。

参考:scanf関数で安全に文字列を読み込む

代替関数の自作

先程のfgets関数による代替処理を関数化したものです、gets関数の代替関数を作る際の参考にしてください。

char *getsn(char *s, int n) {
   if (fgets(s, n, stdin) == NULL) return NULL;
   char *ln = strchr(s, '\n');
   if (ln) *ln = '\0';
   else while (1) {
      int c = getchar();
      if (c == '\n' || c == EOF) break;
   }
   return s;
}
広告