Menu

C言語の scanf:入力の読み取り、&の規則、動かなくなる理由

C言語で scanf を使って入力を読む方法 - & が必要な理由、空白の扱い、%s のバッファオーバーフローと幅指定による対策、戻り値の確認、改行が残るバグ、そして堅牢な代替としての fgets。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

scanf は printf の鏡です。同じヘッダー、似た見た目の書式文字列、そして逆向き。標準入力からテキストを読み、書式に従って変換し、渡されたポインタを通じて結果を格納します。

そしてこれは、C言語のどの関数よりも多くの初心者を混乱させる関数でもあります。3つの挙動が意外で、そのうち1つはセキュリティホールだからです。このページはその4つすべてを扱います。

基本の形、そしてなぜ & なのか

数を打ってEnterを押すと動きます。&age は飾りではありません。Cはすべての引数を値で渡すので、scanf("%d", age) と書けば関数は age の現在の(ゴミの)値のコピーを受け取り、変数そのものには手が届きません。&age は変数のアドレスを渡し、scanf はそこへ書き込みます。全体像はポインタを参照してください。

& を落とすことは scanf で最もよくある間違いです。-Wall を付ければコンパイラが捕まえてくれます(format '%d' expects argument of type 'int *')。警告を有効にしていなければコンパイルは通り、どこかランダムなアドレスのメモリを壊します。

複数の値を読む

1回の呼び出しで複数の値を読めます。入力中の空白 - スペース、タブ、改行 - がそれらを区切り、数値の指定子はその量にかかわらず読み飛ばします。

"%d %d %d" の中のスペースは実は冗長です - %d はすでに先頭の空白を飛ばします - が、書式が読みやすくなります。冗長ではないのは戻り値の確認で、これは次で扱います。

書式中の空白以外の文字は入力と正確に一致しなければなりません。これが構造化された入力を読む方法です。

書式の : は入力にも : を要求します。9:30 と打てば解析され、9 30 と打つと 9 を読んだところで失敗します。

戻り値は必ず確認する

scanf代入に成功した項目の個数を返します - 文字数でも、成功フラグでもありません。何も変換されないまま入力が終わった場合は EOF を返します。

プロンプトで hello と打ってみてください。scanf0 を返し、n手つかずのまま - 最初から入っていたゴミを保持したまま - で、問題のテキストはまだ入力ストリームに残っています。この最後の点が、たった1つの不正な入力を無限ループに変えます。

/* BUG: 数字でない入力に対して永久に回り続ける */
while (scanf("%d", &n) != 1) {
    printf("もう一度: ");
}

scanf は変換できなかったテキストを消費しないので、次の呼び出しも同じ文字で失敗し、それが永遠に続きます。回復するには行の残りを自分で捨てなければなりません。

EOF の確認は重要です。入力ストリームが終わると(ユーザーがCtrl+Dを押す、あるいはパイプで渡したファイルが尽きる)、scanf はその後ずっと EOF を返し続けるので、!= 1 だけを見るループは永遠に再試行してしまいます。「不正な入力」と「もう入力がない」は別々の事態として扱いましょう。それさえできていれば、この discard_line ヘルパーは、人間の入力に scanf を使うどのプログラムでも手元に置いておく価値があります。

%s のバッファオーバーフロー

%s は文字を char 配列へ読み込みます。& がないことに注目してください - 配列名はすでに先頭要素へのポインタに成り下がるので、&name では型が違ってしまいます。

問題は、素の %sあなたの配列の大きさをまったく知らないことです。

char name[10];
scanf("%s", name);      /* 危険: 40文字打たれれば41バイト書き込む */

配列の末尾で書き込みが止まることはありません。メモリ上で次にあるもの - 他の変数や戻り先アドレス - へ突き進んでいきます。これが古典的なバッファオーバーフローで、歴史的にC言語で最も悪用されてきたバグの種類です。対策は %s の間ので、終端の '\0' のための余地を残すため配列サイズより1つ小さくします。

幅は書式文字列の中にリテラルとして書かねばならないので、配列サイズと同期を保つのが面倒です - 実務のCコードの多くが代わりに fgets を使う理由がもう1つ増えるわけです。

%s のもう1つの意外な点:最初の空白で止まります。Ada Lovelace と打つと name には Ada が入り、Lovelace は次の読み取りのためストリームで待機します。%s が読むのは単語であって、行ではありません。

改行が残るバグ

これが誰もが出会う「scanf が動かない」です。数値の指定子は先頭の空白を飛ばしますが、%c は飛ばしません。

そのスペースがないと何が起きるか説明します。30 と打ってEnterを押します。%d30 を消費して改行のところで止まり、'\n' をバッファに残します。次の scanf("%c", &initial) はその改行を文字として読み、即座に返ります - プロンプトが現れ、同じ息で飛ばされるのです。

scanf の書式文字列中のスペースは「ここで任意の量の空白を飛ばせ」という意味なので、" %c" は迷子の改行をまたいで本物の文字を待ちます。このスペースが必要なのは %c%[ の前だけで、他の指定子は自分で空白を飛ばします。

float と char を読む

指定子はおおむね printf と共通ですが、重要な違いが1つあります。scanf はポインタを通じて書き込むので、格納先の正確なサイズを知る必要があるのです。

1.5 2.25 7 x を試してみてください。

%ffloat を、%lfdouble を読みます。printf では float が呼び出し前に double へ昇格されるため両者は同じように表示されますが、scanf に昇格はありません。double *%f を使うと8バイトの変数に4バイトだけ書き込み、壊れた値が残ります。ここの %c に先頭のスペースが不要なのは、直前の %d が改行ではなくスペースで止まっているからです。(完全な表は書式指定子にあります。)

fgets:堅牢な代替

人が打つものは何であれ、1行まるごと読んであとで解析しましょう。fgets はバッファサイズを引数に取るので物理的にオーバーフローし得ず、行が収まったときは改行も消費するので、次の読み取りを混乱させるものが残りません。(バッファより長い行は、その残りが未読のまま残ります - 受け取ったものに '\n' がないことが、それを検出する方法です。)

このパターンについて3点。

  • sizeof line が本物のサイズを渡すので、配列サイズを変えても他を直す必要がありません - %19s のリテラルとは違います。
  • 行が収まった場合 fgets は改行を残します。だから strcspn による切り落としがあるのです。これは同時に、行がバッファに対して長すぎたことを検出する方法でもあります。受け取ったものに '\n' がなければそうです。
  • 解析はあとで。 整数には strtol(解析がどこで止まったかも教えてくれます)、浮動小数点には strtod、あるいは sscanf(line, "%d %d", &a, &b) で、すでに安全に保持している文字列に対して scanf の書式を使えます。

sscanf は独立して言及する価値があります。入力ストリームの代わりに文字列から読む scanf で、fgets と完璧に組み合わさります。scanf の便利な解析を、そのストリーム上の問題なしに手に入れられるのです。

よくある間違い

  • 配列でない引数で**& を忘れる**。-Wall を付けてコンパイルしましょう。
  • 幅のない %s。長い入力を待ち構えるバッファオーバーフローです。
  • 戻り値を無視する。そして scanf が代入しなかった変数を読むことになります。
  • 入力を捨てずに失敗した scanf でループする。無限ループです。
  • 数を読んだあとの先頭スペースなしの %c。プロンプトが飛ばされるバグです。
  • double%f を使う。静かな破壊です。scanf には %lf が必要です。
  • %s が行を読むと期待する。読むのは空白で区切られた1単語です。

よくある質問

なぜC言語の scanf には & が必要なのですか?

Cは引数を値で渡すからです。scanf はあなたの変数を変更しなければならないので、値のコピーではなく変数のアドレスを必要とします:scanf("%d", &age)。唯一の例外は文字配列で、その名前がすでにアドレスなので scanf("%19s", name)& は付きません。

なぜ scanf は char や文字列の入力を飛ばしてしまうのですか?

直前の scanf("%d", ...) が、Enterキーの改行を入力バッファに残したからです。%d%f は先頭の空白を読み飛ばしますが %c は飛ばさないので、待たずにその改行を読んでしまいます。対策は書式の中の空白 - scanf(" %c", &ch) - で、これが先に空白を飛ばすよう scanf に指示します。

C言語で scanf を使って安全に文字列を読むには?

%s にバッファより1つ小さい最大幅を与えます:char name[20]; scanf("%19s", name);。幅がないと %s はユーザーが打った分だけ書き込み、平然と配列の末尾を越えていきます。また %s は最初の空白で止まるので、空白を含むフルネームは読めないことも覚えておいてください。

C言語では scanf と fgets のどちらを使うべきですか?

人が入力するものには fgets を使いましょう。サイズを指定したバッファに1行まるごと読み込むのでオーバーフローし得ず、(行が収まったときは)改行も消費するため、入力が混ざったときの古典的なバグが消えます。バッファより長い行は、その残りが未読のまま入力ストリームに残ります。読んだ行はあとで sscanfstrtol で解析します。scanf は小さな練習や整形された機械入力には問題ありません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める