scanfでよくあるミス

大学のC言語のプログラミング実習で、プログラムに対して数値を入力するために、scanf関数をよく使うと思うのですが、入力した値の格納先を指定する時に変数名に"&"をつけ忘れてしまう人が多いです。最初は慣れないのでよくあることだと思いますが、「printfで変数の中身を出力するときは"&"をつけなくてもいいのに、なぜscanfで入力するときだけ"&"をつけなくていいのか?」という疑問を持つ人が数人いました。そもそも授業ではまだ"&"という記号を変数名に付けるとどうなるのかを習っていないですし、ポインタ等の概念も同様なので、今のところは「そういうもの」として覚えておいてもいいのですが、それでは納得できない、ということもあると思うので、自分なりに説明してみたいと思います。

sample1

入力された数値を出力するだけの簡単なサンプルです。

//sample1.c
#include <stdio.h>
int main(void)
{
    int a;
    scanf("%d",&a);
    printf("a = %d\n",a);
    return (0);
}
10	<-キーボードからの入力
a = 10

のようになります。

sample2

次にscanfで変数名に"&"を付けない場合を試してみましょう。

//sample2.c
#include <stdio.h>
int main(void)
{
    int a;
    scanf("%d",a);
    printf("a = %d\n",a);
    return (0);
}

コンパイルして実行すると…

10	<-キーボードからの入力
Segmentation fault

と表示されてしまいました。環境によっては「セグメントエラー.」などと表示されるかもしれません。

どうしてこうなった

ソースコードで説明するのもいいのですが、コンパイルした後の機械語を見て、どうなっているのか覗いてみることにします。
ということで、逆アセンブルしてみます。

gcc sample1.c -g
objdump a.out -D -S -M intel > sample1.txt
gcc sample2.c -g
objdump a.out -D -S -M intel > sample2.txt

端末でこのように入力すると、逆アセンブル結果がsample1.txtとsample2.txtに格納されます。
scanfに関わる部分だけを抜粋します。

   scanf("%d",&a);
 804841d:   b8 10 85 04 08     mov    eax,0x8048510
 8048422:   8d 54 24 1c        lea    edx,[esp+0x1c]
 8048426:   89 54 24 04        mov    DWORD PTR [esp+0x4],edx
 804842a:   89 04 24           mov    DWORD PTR [esp],eax
 804842d:   e8 16 ff ff ff     call   8048348 <__isoc99_scanf@plt>

    scanf("%d",a);
 804841d:   b8 10 85 04 08     mov    eax,0x8048510
 8048422:   8b 54 24 1c        mov    edx,DWORD PTR [esp+0x1c]
 8048426:   89 54 24 04        mov    DWORD PTR [esp+0x4],edx
 804842a:   89 04 24           mov    DWORD PTR [esp],eax
 804842d:   e8 16 ff ff ff     call   8048348 <__isoc99_scanf@plt>

一行だけ違う部分がありますね。3行目の
lea edx,[esp+0x1c]

edx,DWORD PTR [esp+0x1c]
です。
&aと書いた場合は、
lea edx,[esp+0x1c]
という命令になっています。これは
espという数字を格納する場所(レジスタ)に入っている値と0x1cを足した値をedxに入れるということです。
aと書いた場合は、
edx,DWORD PTR [esp+0x1c]
という命令になっています。これは
espという数字を格納する場所(レジスタ)に入っている値と0x1cを足した値のアドレスに入っている値をedxに入れるということです。

この場合、「edx」というのは、入力された数値を格納するアドレスを表しているので、sample1.cは常に同じところに入力結果が代入されますが、sample2.cは実行する時によってどこに代入されるかわかりません。よってsample2.cは期待した通りの動作をしてくれないということになります。

ちなみに

セグメントエラーというのは、コンパイルする時に想定されていないアドレスにデータを読み書きしようとしたときに起きるエラーです。
sample2.cは実行する時によって入力した値が格納される場所がバラバラなので、ほとんどの場合、本来想定していない場所に数値を書きこもうとしてエラーがでて止まります。
もしもこの仕組みがなければ、このようなバグのあるプログラムを実行して、書き込んだアドレスがシステムの大事な部分だった場合、コンピュータが暴走してしまう危険性があります。
バグのあるプログラムのせいで、コンピュータが暴走してしまうのと、そのプログラムだけがエラーで終了させられるのだったら後者の方がいいに決まっていますよね!
普段使ってるWindowsとか、MacとかLinuxは「セグメント保護」という仕組みが備わっているので、一つのプログラムがおかしな動作をしただけで、簡単にシステム全体に悪影響が及んでしまうことがないようになっています。