はじめに
いきなりだが、以下のx, y, zはどのように表示されるだろうか。
(includeやmainなどは省略)
double x;
int y;
double z;
x = 100 * (1 - 0.8);
y = (int)x;
z = (int)x;
printf("x = %f\n", x);
printf("y = %d\n", y);
printf("z = %f\n", z);
「え、全部20か20.000000でしょ?」と思ったそこのあなた。
私もそう思っている時期がありました。
私の環境(gcc 64bit)では以下のように表示された。
x = 20.000000
y = 19
z = 19.000000
「ああ、19ね。」という人はこの記事を読まなくても多分大丈夫。
「19ってなんじゃそりゃ。」という人は以下へ。
小数の丸め誤差
実は私を惑わせたのは x = 20.000000
だった。
以下の結果を見てほしい。
double x;
x = 100 * (1 - 0.8);
printf("x = %.30f\n", x);
x = 19.999999999999996447286321199499
小数点以下30桁まで表示してみた x である。
そう、20.000000…ではない。
double型(浮動小数点型)をint型(整数型)にキャスト(型変換)してしまうと小数点以下は切り捨てられてしまうので、上の y と z が 19 や 19.000000 になってしまうのは正しい挙動だったのだ。では、なぜ x は20.000000…にならないのか。
(こちら、環境によっては最初のprintfから x = 19.999999
と表示されることもある。この環境だったら迷走することもなかったかもしれない……。@fujitanozomuさんご指摘ありがとうございます。)
小数を2進数で表すのには限界がある
実はこれ、0と1でしか数を表現できないコンピュータの限界により生まれる誤差なのである。
もう少し詳しく説明するために x の計算式を詳しく見ると 0.8
という小数の部分がある。
人間にとってはただの 0.8
だがdouble型で小数点以下30桁まで表示してみる。すると、
double x;
x = 0.8;
printf("x = %.30f\n", x);
x = 0.800000000000000044408920985006
限りなく0.8に近いが0.8より少し大きい近似値になってしまった。
そう、1からこれを引いて100をかけるとroundingError2の x になるのだ。
これには理由がある。
それは、10進数で表せる大抵の小数は0と1の2進数で表すと無限小数になってしまうからだ。
今回の0.8も2進数で表すと以下のようになる。
0.8 ≒ 0.1100110011001100110011001100110011001100110011001101…
当然、double型には無限の桁は扱えない。そこで、上記の "…" のようにある程度の長さで無限小数を丸める。その際に小数は近似値になってしまう。そのまま計算したり、何も考えずint型にキャストしたりすると最初の問題のように理論値との誤差を生む。
このような理由でプログラムの計算値に生まれてしまう誤差を丸め誤差と言う。
printf()の罠
さて、ここで気になるのが最初の表示 x = 20.000000
である。
まるで私は正しいですよと言わんばかりに 20.000000
という人間にとって欲しい答えを表示しているが、私は最初まんまとこれにハマった。
(前章の通り、x = 19.999999
と表示してくれる環境もあるが、ここでは環境に恵まれなかった私の迷走をお送りする。)
「なぜ 20.000000 をintにキャストしているのに y は切り捨てで 20 にならないんだ?」
ここでprintfに小数点以下の桁数を指定して1番上の問題の x と実験的に y = 0.25 , z = 0.26を表示してみる。
double x, y, z;
x = 100 * (1 - 0.8);
y = 0.25;
z = 0.26;
printf("x = %f\n", x);
printf("x = %.1f\n", x);
printf("x = %.10f\n", x);
printf("x = %.30f\n\n", x);
printf("y = %f\n", y);
printf("y = %.1f\n", y);
printf("y = %.10f\n", y);
printf("y = %.30f\n\n", y);
printf("z = %f\n", z);
printf("z = %.1f\n", z);
printf("z = %.10f\n", z);
printf("z = %.30f\n", z);
x = 20.000000
x = 20.0
x = 20.0000000000
x = 19.999999999999996447286321199499
y = 0.250000
y = 0.2
y = 0.2500000000
y = 0.250000000000000000000000000000
z = 0.260000
z = 0.3
z = 0.2600000000
z = 0.260000000000000008881784197001
1番上からデフォルトの小数点以下6桁、小数点以下1桁、小数点以下10桁、小数点以下30桁を x, y, z それぞれ表示してみた。
すると分かるのだが、桁数が少ないと勝手に切り捨てたり切り上げたりで丸めている。
私はprintfの動作に翻弄されていたのである。
調べて分かったのだが、これは処理系によって動作が異なるらしい。
当方のPCはMacBook Pro 2015、gcc 64bit でコンパイルしている。
計算値が理想値と違ったら、とりあえず多めの桁をprintfしてみるのも得策だ。
最後に
そう、これは久々にC言語を復習しようとしていた私が 苦しんで覚えるC言語 練習問題6 問 3-1 でまんまと引っかかった問題である。
以下のようにint型の変数を用意してdouble型キャストで計算させてから代入したのだが19円という結果が出てきたことに「ん?」となってしまった。
int original;
int eightyp;
printf("定価を入力してください。: "); // 100を代入
scanf("%d", &original);
eightyp = (int)(double)(original * (1.0 - 0.8));
printf("8割引: %d円", eightyp); // 8割引: 19円 ?????
「誤差が出ないようにdoubleにキャストしたのになあ」
「1.0 - 0.8の時点で0に切り捨てられている?いやそれだと19にはならないだろう」
「変数をdouble型にしてprintfすると20.000000なんだけどなあ」
などと色々考えた結果、禁断のChatGPT様に初めて質問することにした。
ほぼ完璧な答えが返ってきた。この記事いらないかもしれない。
この答えの中に出てきた「丸め誤差」を調べた結果がこの記事である。
しかし、この答えで1つ解決できなかったものがある。
それが「double型の変数にすると20.000000と答えが表示される」点だ。
これに関しては先述の通りprintfの仕様だったのだが、これにはChatGPT様も混乱していた。
いやでもChatGPTすごいなあ……。
基礎を疎かにしてはいけないと身に沁みたので反省も含めて1記事書きました。
間違いなどありましたらご指摘いただけると嬉しいです。
参考記事・サイト
丸め誤差
・ Cプログラミング(3) 除算と計算の誤差
・ プログラムによる小数点以下の計算で誤差が生じる原因と対処法2選
・ 浮動小数点数型と誤差
10進数の小数を2進数に変換する
printfの丸め
Special Thanks
・ ChatGPT