声明
printf("%f\n",0.0f);
0を出力します。
ただし、ステートメント
printf("%f\n",0);
ランダムな値を出力します。
私はある種の未定義の振る舞いを示していることを理解していますが、具体的な理由を理解することはできません。
すべてのビットが0である浮動小数点値は、値が0の有効なfloat
のままです。float
とint
は、マシン上で同じサイズです(関連がある場合)。
printf
で浮動小数点リテラルの代わりに整数リテラルを使用すると、この動作が発生するのはなぜですか?
追伸私が使用すると同じ動作が見られます
int i = 0;
printf("%f\n", i);
_"%f"
_形式には、double
型の引数が必要です。タイプint
の引数を指定しています。そのため、動作は未定義です。
この規格は、すべてのビットがゼロであることを_0.0
_(多くの場合はそうですが)、またはdouble
値、またはint
およびdouble
は同じサイズです(double
ではなくfloat
であることに注意してください)、または同じサイズであっても、引数として可変引数関数に渡される同じ方法。
システムで「動作する」ことがあります。これは、エラーの診断が困難になるため、未定義の動作の最悪の症状です。
N157 7.21.6.1パラグラフ9:
...いずれかの引数が対応する変換仕様の正しいタイプでない場合、動作は未定義です。
タイプfloat
の引数はdouble
に昇格されるため、printf("%f\n",0.0f)
が機能します。 int
よりも狭い整数型の引数は、int
または_unsigned int
_に昇格されます。これらのプロモーションルール(N1570 6.5.2.2パラグラフ6で指定)は、printf("%f\n", 0)
の場合には役立ちません。
最初に、いくつかの他の答えで触れましたが、私の心には十分に明確に綴っていませんでした:doesはmostライブラリ関数がdouble
またはfloat
引数を取るコンテキスト。コンパイラは自動的に変換を挿入します。たとえば、sqrt(0)
は明確に定義されており、sqrt((double)0)
とまったく同じように動作し、そこで使用される他の整数型の式についても同じことが言えます。
printf
は異なります。異なる数の引数を取るため、異なります。その関数プロトタイプは
_extern int printf(const char *fmt, ...);
_
したがって、あなたが書くとき
_printf(message, 0);
_
コンパイラーは、2番目の引数がどのタイプprintf
expectsであるかについての情報を持っていません。渡される引数式の型(int
)のみがあります。したがって、ほとんどのライブラリ関数とは異なり、引数リストがフォーマット文字列の期待に一致することを確認するのはプログラマーです。
(現代のコンパイラcanは、フォーマット文字列を調べて、型の不一致があることを伝えますが、変換を挿入し始めません何年も後に、あまり役に立たないコンパイラーで再構築されたときよりも、あなたのコードがすぐに壊れるはずです。
さて、質問の残りの半分は:(int)0と(float)0.0がほとんどの最新システムで、両方とも32ビットとして表され、すべてゼロであると仮定すると、どうして偶然に動作しないのでしょうか? C規格では、「これは動作する必要はありません。自分で実行してください」と書かれていますが、動作しない最も一般的な2つの理由を説明します。それはおそらくあなたが理解するのに役立つでしょうなぜそれは必須ではありません。
まず、歴史的な理由により、float
を可変引数リストに渡すと、double
にpromotedが取得されます。ほとんどの最新システムでは、64ビット幅です。したがって、printf("%f", 0)
は、32個のゼロビットのみを64個の期待される呼び出し先に渡します。
同様に重要な2番目の理由は、浮動小数点関数の引数が整数の引数とは異なるplaceで渡される可能性があることです。たとえば、ほとんどのCPUには整数と浮動小数点値用に別々のレジスタファイルがあるため、引数0から4は、整数の場合はレジスタr0からr4に、浮動小数点の場合はf0からf4に入れられるという規則になります。したがって、printf("%f", 0)
はレジスターf1でそのゼロを探しますが、まったくありません。
浮動小数点数リテラルの代わりに整数リテラルを使用すると、この動作が発生するのはなぜですか?
printf()
には、最初のパラメーターとしてconst char* formatstring
以外に型付きパラメーターがないためです。残りのすべてにcスタイルの省略記号(...
)を使用します。
書式文字列で指定された書式タイプに従って、そこに渡された値を解釈する方法を決定するだけです。
あなたはしようとするときと同じ種類の未定義の振る舞いをするでしょう
int i = 0;
const double* pf = (const double*)(&i);
printf("%f\n",*pf); // dereferencing the pointer is UB
通常、double
を期待する関数を呼び出すが、int
を指定すると、コンパイラーは自動的にdouble
に変換します。引数の型が関数プロトタイプで指定されていないため、printf
では発生しません。コンパイラーは変換を適用する必要があることを知りません。
一致しないprintf()
指定子"%f"
と入力(int) 0
は未定義の動作につながります。
変換仕様が無効な場合、動作は未定義です。 C11dr§7.21.6.19
UBの原因の候補。
それは仕様ごとのUBであり、コンパイルは普通ではありません-'nufは言いました。
double
とint
はサイズが異なります。
double
とint
は、異なるスタックを使用して値を渡すことができます(一般的なvs. [〜#〜] fpu [〜#〜] スタック。)
double 0.0
mightは、すべてゼロのビットパターンでは定義されません。 (まれ)
これは、コンパイラの警告から学ぶ絶好の機会の1つです。
$ gcc -Wall -Wextra -pedantic fnord.c
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
printf("%f\n",0);
^
または
$ clang -Weverything -pedantic fnord.c
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
printf("%f\n",0);
~~ ^
%d
1 warning generated.
したがって、printf
は、互換性のないタイプの引数を渡すため、未定義の動作を引き起こします。
何が混乱するのか分かりません。
フォーマット文字列にはdouble
が必要です。代わりにint
を提供します。
2つの型のビット幅が同じであるかどうかはまったく関係ありませんが、このような壊れたコードからハードメモリ違反の例外が発生するのを防ぐのに役立つ場合があります。
なぜ正式にUBであるかは、いくつかの回答で議論されています。
この動作を具体的に取得する理由はプラットフォームに依存しますが、おそらく次のとおりです。
printf
は、標準の可変引数伝播に従って引数を期待します。つまり、float
はdouble
になり、int
より小さいものはint
になります。int
を予期しているdouble
を渡している。 int
はおそらく32ビット、double
は64ビットです。つまり、引数が置かれるはずの場所から始まる4つのスタックバイトは0
が、次の4バイトには任意のコンテンツがあります。それは、表示される値を構築するために使用されるものです。_"%f\n"
_は、2番目のprintf()
パラメーターのタイプがdouble
である場合にのみ、予測可能な結果を保証します。次に、可変個引数関数の追加の引数は、デフォルトの引数昇格の対象です。整数の引数は整数の昇格の対象になり、浮動小数点型の値にはなりません。また、float
パラメーターはdouble
に昇格されます。
さらに、標準では、2番目の引数がfloat
またはdouble
になり、それ以外は何も許可されません。
この「未定値」問題の主な原因は、int
変数パラメーターセクションに渡されるprintf
値のポインターのキャストが、_va_arg
_マクロが実行するdouble
タイプのポインターへのキャストにあります。
double
サイズのメモリバッファ領域がint
サイズより大きいため、printfにパラメータとして渡された値で完全に初期化されなかったメモリ領域への参照が発生します。
したがって、このポインターが逆参照されると、未定義の値、またはprintf
にパラメーターとして渡された値の一部を含む「値」が返され、残りの部分は別のスタックバッファー領域またはコード領域からも取得できます(メモリフォールト例外を発生させる)、実際のバッファオーバーフロー。
「printf」および「va_arg」のコード化実装のこれらの特定の部分を考慮することができます...
printf
_va_list arg;
....
case('%f')
va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
....
_
ダブル値パラメータのコードケース管理のvprintfでの実際の実装(gnu impl。を考慮):
_if (__ldbl_is_dbl) { args_value[cnt].pa_double = va_arg (ap_save, double); ... }
_
va_arg
_char *p = (double *) &arg + sizeof arg; //printf parameters area pointer
double i2 = *((double *)p); //casting to double because va_arg(arg, double)
p += sizeof (double);
_
references