C言語のscanf()で文字列を読み取る方法

349, 2021-11-23

目次

C言語のscanf()で文字列を読み取る

C言語にはscanf()という書式で入力を読める関数があります
今回はこのscanf()で文字列を入力する方法を解説します。

競技プログラミングなどではscanf()は広く使われています。
それはscanf()は手軽で入力を簡単に読めるからです。

scanf()の文字列の入力方法を覚えれば書ける処理も多くなります。
今回は具体的に↓のコンテンツを見ていきます。

  • scanf()で文字列「Hello」を入力する

  • scanf()で複数の文字列を入力する

  • scanf()で空白文字を含む文字列を入力すると?

  • scanf()のスキャン集合

  • スキャン集合を使って改行以外の文字列を入力する

  • scanf()のバッファーオーバーフロー

  • scanf()のバッファーオーバーフローの予防

  • scanf()は安全な関数か?

scanf()で文字列「Hello」を読み取る

scanf()標準入力から入力を読み込み、第1引数のフォーマットで入力をパースして、第2引数以降の変数に保存します
scanf()で「Hello」という入力を読むコードは↓のように書きます。

#include <stdio.h>

int main(void) {
    char buf[20];

    scanf("%s", buf);

    printf("buf[%s]\n", buf);
    // buf[Hello]

    return 0;
}

scanf()第1引数に「"%s"」というフォーマットを指定します
そして第2引数に文字配列のbufを指定します
これは標準入力から読み取った入力を「%s」でパースしてbufに保存するという意味です。
%s」は文字列をパースするのに使われる変換指定です。

プログラムをコンパイルして実行すると、端末が入力待ちになります。
これはscanf()が入力を待っている状態です。
その状態でキーボードで「Hello」と入力し、エンターキーを押します。
そうするとscanf()が入力を読み取って、その入力を文字配列bufに保存します。

$ ./prog
Hello
buf[Hello]

printf()bufを出力すると結果は「buf[Hello]」になります。
今回は「Hello」という入力を読みましたが、これは別に「World」などでもかまいません。

文字列とアドレス演算子

scanf()の使い方をご存知な方はscanf()で整数を入力したことがあると思います。
その場合、scanf()は↓のようなコードになります。

    int num;
    scanf("%d", &num);  // アドレス演算が必要

scanf()の第2引数以降の引数には変数のアドレスを渡す必要があります
そのため↑の変数numもアドレス演算子(&)でアドレスを取り出しています。

文字配列の場合はアドレス演算子は必要ありません
これは文字配列を普通に参照した場合、配列の先頭アドレスを参照することになるからです。

    char s[20];
    scanf("%s", s);  // アドレス演算は必要ない

scanf()で複数の文字列を入力する

scanf()の第1引数のフォーマットには複数の変換指定子を書くことができます。
たとえば「Cat Dog」という入力があったとします。
この入力をCatはCat, DogはDogで別々の変数に保存したいとします。
その場合、scanf()を使うと↓のようになります。

#include <stdio.h>

int main(void) {
    char cat[20];
    char dog[20];

    scanf("%s %s", cat, dog);

    printf("cat[%s]\n", cat);  // Cat
    printf("dog[%s]\n", dog);  // Dog
    return 0;
}

プログラムをコンパイルして実行し、キーボードから「Cat Dog」を入力します。
すると↑のscanf()はその「Cat Dog」をフォーマット「%s %s」でパースします。
その結果はscanf()の引数のcatdogに保存されます。

$ ./prog
Cat Dog
cat[Cat]
dog[Dog]

変換指定子(%sなど)の順番とフォーマット以降の引数の順番は合致している必要があります
つまり↑の場合、最初の%sは変数catに対して処理を行い、2つ目の%sは変数dogに対して処理を行います。

scanf()で空白文字を含む文字列を入力すると?

変換指定子「%s」は空白文字を読み取れません
ためしに先ほどのプログラムで「Hello, World!」という入力を行ってみてください。
結果は「Hello,」になると思います。
これはカンマの次の半角スペースで読み込みがストップしているために起こります。

scanf()で半角スペースも含んだ入力を読み取りたい場合は「スキャン集合」というものを理解しておく必要があります。
次からスキャン集合について解説します。

scanf()のスキャン集合

scanf()の第1引数をフォーマットといいます。
そしてフォーマットに指定できる指定を変換指定子と言います。
先ほどの例で言うと「%s」が変換指定子です。

スキャン集合とはこの変換指定子の一種です。
スキャン集合を使うと色々な入力を制限したり逆に読み込めるようになったりします

スキャン集合は変換指定では「%[]」のように書きます。
角カッコの中に様々な設定を書くことで、その動作を変更することができます。

スキャン集合はまとめると↓のようなことが可能です。

  • 読み込む文字の指定

  • 範囲指定

  • 否定

読み込む文字の指定

たとえば入力から「abc」だけを読み取りたい場合があるとします。
その場合はスキャン集合を「%[abc]」と書きます。

#include <stdio.h>

int main(void) {
    char buf[20];

    scanf("%[abc]", buf);

    printf("buf[%s]\n", buf);
    // buf[abc]
    return 0;
}

↑のコードを実行すると入力待ちになります。
キーボードから「abcdef」と入力しエンターキーを押します。
そうするとbufにはabcの文字だけが保存されます

$ ./prog
abcdef
buf[abc]

範囲指定

スキャン集合では範囲指定をすることができます。
たとえば「aからzまでのアルファベット」だけを読み取りたいとします。
その場合は「%[a-z]」というスキャン集合を書きます。

#include <stdio.h>

int main(void) {
    char buf[20];

    scanf("%[a-z]", buf);

    printf("buf[%s]\n", buf);
    // buf[abcxyz]
    return 0;
}

キーボードから「abcxyz123」と入力します。
するとbufには「abcxyz」だけが保存されます。

$ ./prog
abcxyz123
buf[abcxyz]

否定

スキャン集合では文字の指定に否定が使えます。
否定は「^」で書かれます。
たとえば「xyz以外の文字列を読み込みたい」となった場合、スキャン集合は「%[^xyz]」と書きます。

#include <stdio.h>

int main(void) {
    char buf[20];

    scanf("%[^xyz]", buf);

    printf("buf[%s]\n", buf);
    // buf[abc]
    return 0;
}

↑のコードをコンパイルして実行すると入力待ちになります。
入力から「abcxyz」と入力します。
するとbufには「abc」が保存されます。

$ ./prog
abcxyz
buf[abc]

スキャン集合の指定は「xyz以外の文字列を読み込む」ですので、入力からabcだけが残り、xyzは読み捨てられることになります。

スキャン集合を使って改行以外の文字列を入力する

先ほどのスキャン集合の「否定」を使うと「改行以外の文字列を読み込む」というスキャン集合を書くことができます
その場合、スキャン集合は「%[^\n]」になります。

このスキャン集合を使えばscanf()で半角スペースを含んだ文字列を読み込むことが可能になります。

#include <stdio.h>

int main(void) {
    char buf[20];

    scanf("%[^\n]", buf);

    printf("buf[%s]\n", buf);
    // buf[Hello, World!]
    return 0;
}

↑のコードをコンパイルして実行すると入力待ちになります。
キーボードから「Hello, World!」と入力します。
するとbufには「Hello, World!」が保存されます。

$ ./prog
Hello, World!
buf[Hello, World!]

「Hello, World!」は半角スペースを含んだ文字列ですが、半角スペースも一緒に読み取れてるのがわかります。
スキャン集合の指定は「改行以外」なので、タブなども読み取ることが可能です。

scanf()のバッファーオーバーフロー

便利なscanf()ですが、この関数は使い方を誤るとバッファーオーバーフローを起こすことがあります
たとえば要素数が5の文字配列bufを確保したとします。
そしてそのバッファにscanf()で文字列を入力します。

#include <stdio.h>

int main(void) {
    char buf[5];

    scanf("%s", buf);

    printf("buf[%s]\n", buf);
    return 0;
}

↑のコードをコンパイルして実行すると入力待ちになります。
そして入力に5文字以上の長い文字列を与えます。
そうすると場合によってはセグフォ(Segmentation fault)になります。
これはbufに対してサイズ以上の入力を与えたためです。

これの怖いのは、入力と環境によってはセグフォが発生しないということです。
バッファーオーバーフローが発生しているのは確かですが、それを検知出来ないということです。
この辺はC言語の怖い所です。

C言語の怪談

C言語こわい

scanf()のバッファーオーバーフローの予防

変換指定子を使うと読み込む入力幅を指定することができます。
たとえば「読み込みは4字まで」という場合は「%4s」と書きます。

#include <stdio.h>

int main(void) {
    char buf[5];

    scanf("%4s", buf);

    printf("buf[%s]\n", buf);
    // buf[Hell]
    return 0;
}

↑の場合、bufのサイズは5です。
そして変換指定子は「%4s」なので4文字まで読み込む指定になっています。
なぜ5文字でなく4文字なのかというと、最後の1文字はナル文字のために残してあるからです。
scanf()は自動でナル文字を付加します。

コードをコンパイルして実行し、キーボードから「Hello」と入力します。
すると「Hello」の先頭4文字だけが読み込まれ、bufに保存されます。
bufの結果は「Hell」になります。

$ ./prog
Hello
buf[Hell]

このように変換指定子に入力幅を指定すればバッファーオーバフローは防ぐことができます。
(マルチバイト文字列の場合)

スキャン集合とあわせる場合は↓のように書きます。

#include <stdio.h>

int main(void) {
    char buf[20];

    scanf("%19[^\n]", buf);

    printf("buf[%s]\n", buf);
    // buf[Hello, World!]
    return 0;
}

↑の場合、スキャン集合は「[^\n]」です。入力幅は19になっています。
こうすると「改行以外の文字列を19文字だけ読み込む」という指定が可能になります。

scanf()は安全な関数か?

しかし、変換指定子に入力幅を指定してバッファーオーバフローを防げると言ってもヒューマンエラーは防げません
バッファのサイズと入力幅が違っていた……なんてことは往々にしてありえることです。
つまりバッファーオーバーフローの危険性が依然として存在するということです。
これはC言語の関数全般に言えることですが、C言語の関数は使い方を間違えると簡単に脆弱性が生まれます
これはC言語の言語構造上しかたのないことではあります。
そのためC言語の扱いには熟練さが要求されます。

scanf()は変換指定子の扱いが複雑で取り扱いには熟練さが必要とされます。
そのため初心者の方には扱いづらい関数と言えるかもしれません。
しかし、慣れると入力を簡単に読むことができるので、競技プログラミングなどでは多用される傾向があります。

文字列の入力を読みたい場合は、初心者の方はscanf()よりもfgets()を使ったほうがいいでしょう
fgets()は非常にシンプルな関数です。
またセキュアなプログラムを書く場合はscanf()よりもfgets()が多用される傾向があります。

おわりに

今回はC言語のscanf()で文字列を読み取る方法を解説しました。
scanf()は扱いが少々難しいですが、慣れると簡単に入力を読むことが出来る関数です。
セキュアな文字列入力ではscanf()よりもfgets()を使いましょう。

スキャンしてあげる

電子の波が身体を駆け巡る