

C言語初学者です。

以下のように、`int` で変数を用意して、`double` の入力変換指定子で値を入力し、`double` の入力変換指定子で値を出力した場合、なぜ
`0.000000` になるのか疑問に思っています。

[code]

    #include <stdio.h>
    
    int main(void)
    {
        int data;
        scanf("%lf", &data); // 実数入力
        printf("%f\n", data);
        return 0;
    }
    
[/code]

上記が誤ったコード（`int` で `data` を定義しているのが悪い）であることは理解しているのですが、挙動の理解をしたいと思っています。

`data` は `int` でメモリ確保されているので、そこに `double`
で入力するとメモリが後ろにあふれてしまっているのかなと想像しているのですが、それだと `printf` の際になぜ `0.000000`
となるのか説明できないなと悩んでいます（それだと意味不明な数値の羅列になりそうな気がしていました）。

コンパイラは `gcc (MinGW.org GCC Build-2) 9.2.0` です。  
よろしくお願いします。


CC BY-SA 4.0
2022-02-27T02:46:04.283
86592
2023-05-04T06:49:15.610
2023-05-04T06:24:31.277
4236
29953
question
windows c mingw
int 型の変数に double でキーボード入力した際の挙動について
  
  
キーボードの入力とかは関係なく、intの0を`%f`で指定したから`0.000000`と表示されています。  
`printf("%f\n", 0);`と書いたのと同じです。


CC BY-SA 4.0
2022-02-27T04:50:55.233
86595
2022-02-27T04:50:55.233
805
86592
answer


`gcc (MinGW.org GCC Build-2) 9.2.0`とのことで、32bit Windows（64bit
Windowsだとしても32bit実行ファイルが作成・実行される）でしょうか。C言語では、データ型のサイズを具体的に定めていないため、環境によって異なります。そのため、質問のように意図的にデータ型を誤った場合、環境ごとのサイズに応じた挙動を示します。

その上で、32bit Windowsにおいては想像されている通りです。意味不明な数値の羅列になり得ます。

`printf()`を呼び出す際に`data`をスタックに格納します。`data`は32bit
int型なのでスタックには32bitしか書き込まれていません。しかし呼び出された`printf()`は`%f`の指示により64bit倍精度浮動小数点数と解釈して読み取ります。このため、スタックに格納された`data`の隣32bit分を読み出そうとします。

[code]

    0         1         2         3         4         5         6
    0123456789012345678901234567890123456789012345678901234567890123
    <-------------------- fraction --------------------><-exponent>S
    <---------- int data ----------><----------- 未初期化 ---------->
    ※ S = sign
    
[/code]

この図の状況で、`int
data`に何が格納されていようが、表示する精度を上げない限り表示には使われません。それよりも`printf`には渡していない未初期化部分が

  * 符号部 1ビット
  * 指数部 11ビット
  * 仮数部 上位20ビット

に当たり表示内容を決定づけています。

短いテストプログラムですし、スタックが汚れておらず未初期化部分が偶然`0`で埋められていたのだと思います。結果的に倍精度浮動小数点数として`0.000000`と解釈できるビット列になっただけです。

ちなみに、意図的に`data`の隣にも別の値を書き込むと表示が変化することを確認できるかと思います。

[code]

    for (int i = 0; i < 32; i++)
      printf("i = %d, %f\n", i, data, 1 << i);
    
[/code]

を手元のVisual C++で実行したところ、次のような出力が得られました。

[code]

    i = 0, 0.000000
    i = 1, 0.000000
    i = 2, 0.000000
    i = 3, 0.000000
    i = 4, 0.000000
    i = 5, 0.000000
    i = 6, 0.000000
    i = 7, 0.000000
    i = 8, 0.000000
    i = 9, 0.000000
    i = 10, 0.000000
    i = 11, 0.000000
    i = 12, 0.000000
    i = 13, 0.000000
    i = 14, 0.000000
    i = 15, 0.000000
    i = 16, 0.000000
    i = 17, 0.000000
    i = 18, 0.000000
    i = 19, 0.000000
    i = 20, 0.000000
    i = 21, 0.000000
    i = 22, 0.000000
    i = 23, 0.000000
    i = 24, 0.000000
    i = 25, 0.000000
    i = 26, 0.000000
    i = 27, 0.000000
    i = 28, 0.000000
    i = 29, 0.000000
    i = 30, 2.000000
    i = 31, -0.000000
    
[/code]

* * *

なお、MinGW-w64など64bit Windows ~~やmetropolisさんが説明されている64bit Linux~~
においてはまた異なる挙動になります。  
64bit Windows ~~および64bit Linux~~ においては、32bit
int型をスタックに格納する際、64bit領域を使います。この際の未使用の32bit部分はゼロクリアされます。  
`printf()`はこの64bit領域を[倍精度浮動小数点数](https://ja.wikipedia.org/wiki/%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0#IEEE%E6%96%B9%E5%BC%8F%EF%BC%88IEEE_754_%E5%BD%A2%E5%BC%8F%EF%BC%89)として読み取ろうとしますが、ゼロクリアされた未使用の32bit部分は

  * 符号部 1ビット = 0
  * 指数部 11ビット = 0
  * 仮数部 上位20ビット = 0
  * 仮数部 下位32ビット = dataの値

に該当し、「指数部、仮数部ともに 0 のときは ±0 を表す」が適用されるため、常に`0.000000`が表示されます。

前述のコードについても、64bitでは`data`の隣に`1 << i`を書き込んでいますが説明の通り参照されないため、

[code]

    i = 0, 0.000000
    i = 1, 0.000000
    i = 2, 0.000000
    i = 3, 0.000000
    i = 4, 0.000000
    i = 5, 0.000000
    i = 6, 0.000000
    i = 7, 0.000000
    i = 8, 0.000000
    i = 9, 0.000000
    i = 10, 0.000000
    i = 11, 0.000000
    i = 12, 0.000000
    i = 13, 0.000000
    i = 14, 0.000000
    i = 15, 0.000000
    i = 16, 0.000000
    i = 17, 0.000000
    i = 18, 0.000000
    i = 19, 0.000000
    i = 20, 0.000000
    i = 21, 0.000000
    i = 22, 0.000000
    i = 23, 0.000000
    i = 24, 0.000000
    i = 25, 0.000000
    i = 26, 0.000000
    i = 27, 0.000000
    i = 28, 0.000000
    i = 29, 0.000000
    i = 30, 0.000000
    i = 31, 0.000000
    
[/code]

となります。

* * *

metropolisさんが説明されている64bit Linuxの場合、`int data`が64bitになるので更に異なる挙動を示しますね。


CC BY-SA 4.0
2022-02-27T05:15:58.587
86596
2023-05-04T06:49:15.610
2023-05-04T06:49:15.610
4236
4236
86592
answer


> data は int でメモリ確保されているので、そこに double で入力するとメモリが後ろにあふれてしまっているのかなと想像しているのですが、

glibc scanf(3) の実装は以下の様になっています(`long double *` へ cast)。なので、その通りです。

[glibc/stdio-common/vfscanf-
internal.c](https://code.woboq.org/userspace/glibc/stdio-common/vfscanf-
internal.c.html#2437)

[code]

    if ((flags & LONGDBL) \
        && __glibc_likely ((mode_flags & SCANF_LDBL_IS_DBL) == 0))
      {
        long double d = __strtold_internal
          (char_buffer_start (&charbuf), &tw, flags & GROUP);
        if (!(flags & SUPPRESS) && tw != char_buffer_start (&charbuf))
          *ARG (long double *) = d;
      }
    
[/code]

> それだと printf の際になぜ 0.000000 となるのか説明できないなと悩んでいます

`data` への pointer(`int *`型)を `long double *` に cast して dereference
すれば入力した値(float の値)が表示されます(stack を踏み抜きますが)。

ではなぜ `%f` を指定すると `0.000000` が **表示される** のか、に関しては以下の warning message と
`printf(3)` のソースコード([glibc/stdio-common/vfprintf-
internal.c](https://code.woboq.org/userspace/glibc/stdio-common/vfprintf-
internal.c.html#777))を眺めると判るかと思います。

**サンプルコード**

[code]

    #include <stdio.h>
    #include <limits.h>
    
    int main(void)
    {
      int data;
      scanf("%lf", &data); // 実数入力
    
      printf("as is: %d\n", data);
      printf("as double: %f\n", data);
      printf("cast to double: %f\n", (double)data);
      printf("cast to double type pointer: %f\n", *(double * )&data);
    
      printf("woops!: %f\n", 0, data);
    
      return 0;
    }
    
[/code]

**実行結果**

[code]

    $ uname -isr
    Linux 5.15.0-18-generic x86_64
    $ gcc --version
    gcc (Ubuntu 11.2.0-16ubuntu1) 11.2.0
    $ /lib/x86_64-linux-gnu/libc.so.6
    GNU C Library (Ubuntu GLIBC 2.35-0ubuntu1) stable release version 2.35.
    
    $ gcc -Wall -Wextra x.c -o x
    x.c: In function ‘main’:
    x.c:7:12: warning: format ‘%lf’ expects argument of type ‘double *’, but argument 2 has type ‘int *’ [-Wformat=]
        7 |   scanf("%lf", &data); // 実数入力
          |          ~~^   ~~~~~
          |            |   |
          |            |   int *
          |            double *
          |          %d
    x.c:10:23: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
       10 |   printf("as double: %f\n", data);
          |                      ~^     ~~~~
          |                       |     |
          |                       |     int
          |                       double
          |                      %d
    x.c:14:20: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
       14 |   printf("woops!: %f\n", 0, data);
          |                   ~^     ~
          |                    |     |
          |                    |     int
          |                    double
          |                   %d
    x.c:14:10: warning: too many arguments for format [-Wformat-extra-args]
       14 |   printf("woops!: %f\n", 0, data);
          |          ^~~~~~~~~~~~~~
    
    $ ./x
    1.234
    as is: -927712936
    as double: 0.000000
    cast to double: -927712936.000000
    cast to double type pointer: 1.234000
    woops!: 1.234000
    *** stack smashing detected ***: terminated
    Aborted (core dumped)
    
[/code]

※ stack protector が有効になっているので異常終了します。

という訳で、Fushihara さんの回答はある意味で正しいと言えます(多少説明不足かな、とは思いますが……)。


CC BY-SA 4.0
2022-02-27T09:45:03.577
86598
2022-02-27T10:10:04.267
2022-02-27T10:10:04.267
47127
47127
86592
answer