【C言語】scanf関数で安全に文字列を読み込む方法

scanf関数にはバッファオーバーフロー/バッファオーバーランの危険性があります。

char s[3];
scanf("%s", s); // 2文字以上入力出来てしまう

本来、上記のコードは終端文字\0を除いて最大2文字までの入力しか受け付けられないはずのコードですが、実際には無制限に入力出来てしまいます。

仮にコンソール上で2文字以上の入力を行ってしまうと(例えば abcd)、配列sには{'a', 'b', 'c', 'd', '\0'}という値が書き込まれてしまい、'd', '\0'の2バイトが余分に書き込まれてしまいます。場合によってはメモリ領域が破壊されてしまう危険性もあるため注意が必要です。

対処方法

この問題に対処するためには、以下のテクニックを用いると良いです。

char s[3];
scanf("%2s%*[^\n]", s);
getchar();

これはscanfで文字列を安全に読み込むためのちょっとしたイディオムなのですが、詳しい解説は次の節で行います。

他にも# scanf_s関数やfgets関数を活用する方法もありますので、検討してみてください。

フィールド幅を指定する

%幅sという形で文字列指定子に対して幅指定を行うことが出来ます。これによって指定された幅以上の読み取りが行われなくなります。

char s[3];
scanf("%2s", s);

上記コードで4文字入力(abcd)を行った場合には、配列に対して{'a', 'b', '\0'}という値が書き込まれることになります。

ただし、この方法は万能ではなく、残りの入力文字cdおよび改行コードが入力ストリーム上に残ってしまうという問題があります。この問題は後続の入力処理にも影響するため注意が必要です。

char s[3];
scanf("%2s", s);    // 入力: "abc\n"
printf("%s", s);    // 出力: "ab"
putchar(getchar()); // 出力: "c"
putchar(getchar()); // 出力: "\n"

読み飛ばしする

この問題への対処としては読み飛ばしのテクニックが有効です。

char s[3];
scanf("%2s%*[^\n]%*c", s); // 入力: "abc\n"
printf("%s", s);           // 出力: "ab"
putchar(getchar());        // 入力待ち

%*[^\n]で改行までの入力を読み飛ばします。また%*cで残りの改行コードを更に読み飛ばしています。これで後続の入力処理への影響を抑えることが出来ます。

この*フラグは代入抑止と呼ばれるもので、「読み取りは行うが、実引数への代入は行わない」ことを指示することが出来ます。

ただし、この方法も万能ではなく、指定したフィールド幅と同じ文字数またはより少ない文字が入力された場合に、入力ストリーム上に改行コードだけが残ってしまうという問題が残ります。

char s[3];
scanf("%2s%*[^\n]%*c", s); // 入力: "a\n" | "ab\n"
printf("%s", s);           // 出力: "a"   | "ab"
putchar(getchar());        // 出力: "\n"  | "\n"

空読みする

そのため必要に応じてgetchar()関数による改行コードの空読みが必要になります。

char s[3];
scanf("%2s%*[^\n]%*c", s); // 入力: "a\n"
printf("%s", s);           // 出力: "a"
getchar();                 // 空読み
putchar(getchar());        // 入力待ち

ただし、この空読みの方法も万能ではありません。空読みが必要な入力とそうでない入力をプログラム側で判定することが困難であるためです。

そのため改行コードの空読みはscanfの後に行うようにする必要があります。

char s[3];
scanf("%2s%*[^\n]", s); // 入力: "a\n" | "ab\n"
getchar();              // 空読み
printf("%s", s);        // 出力: "a"   | "ab"
putchar(getchar());     // 入力待ち

空白文字を含む

空白文字と空文字の読み取りを容認する場合は次のような処理が必要になります。これによってgets関数と同等の挙動が実現できます。

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

scanfの戻り値を活用する

もう一つの方法として、scanf関数の戻り値を活用する方法もあります。

char s[3], c;  // このchar c変数はダミー用
int n = scanf("%2s%*[^\n]%c", s, &c); // 入力: "a\n" | "ab\n"
if (n == 1) getchar();                // 空読み
printf("%s", s);                      // 出力: "a"   | "ab"
putchar(getchar());                   // 入力待ち

scanfの戻り値は、代入に成功した実引数の個数を返します。そのため改行コード読み取り用のダミーの実引数&cを指定し、その際のscanfの戻り値を判定することで、改行コードの有無を知ることが出来ます。

scanf内で改行コードが読み取られなかった場合には、実引数sに対する代入のみが行われるため、scanfの戻り値は1となります。また改行コードが読み取られた場合には実引数s&cに対する代入が行われるため、戻り値は2となります。

fflush(stdin)の利用について

fflush(stdin)で入力ストリームのバッファクリアを行う方法も知られていますが、C言語の規格上は未定義動作のコードであり、処理系依存の挙動をとるため注意が必要です。

rewind(stdin)の利用について

rewind(stdin)fseek(stdin, 0L, SEEK_SET);で入力ストリームのファイルポインタを先頭に移動させて入力状態をリセットさせる方法も知られていますが、標準入力へのリダイレクト/パイプ利用時の挙動には十分な注意が必要となります。scanf関数の代入抑止が利用できる環境ではそちらの利用をオススメします。

scanf_s関数

Windows環境の場合はscanf_s関数を用いることもできます。第三引数にバッファサイズを指定します。

char s[3];
scanf_s("%s", s, 3);
広告
広告