浮動小数点型 解答ページ | Programming Place Plus C言語編 第20章

トップページC言語編第20章

問題①

問題① 次のプログラムを、出力される結果が 1.0 になるように、誤差を考慮した作りに修正してください。

#include <stdio.h>

int main(void)
{
    float n = 0.0f;

    for (int i = 0; i < 100; ++i) {
        n += 0.01f;
    }

    printf("%f\n", n);
}


0.0 で初期化した変数 n に、0.01 を 100回加算しています。まずこのまま実行してみると、次の結果が得られます。

実行結果:

0.999999

誤差が出ています。この誤差をなくすには、本編で解説したように、0.01 を 100回加算するのではなく、「0.01×100」したものを、1回だけ加算することです。

#include <stdio.h>

int main(void)
{
    float n = 0.0f;

    n = n + 0.01f * 100;

    printf("%f\n", n);
}

実行結果:

1.000000

なお、特別に事情がなければ、float型より double型を使いましょう。

問題②

問題② 問題①のプログラムにおいて、変数 n の値がどのように変化しているかを調べて、何が起きているか説明してください。


0.01 の加算を行うたびに n の値を出力してみると、浮動小数点数の表現が移り変わる様子が見えます。printf関数の %e 変換指定子を使うと良いでしょう。

#include <stdio.h>

int main(void)
{
    float n = 0.0f;

    for (int i = 0; i < 100; ++i) {
        n += 0.01f;
        printf("%e\n", n);
    }
}

実行結果:

1.000000e-02
2.000000e-02
3.000000e-02
4.000000e-02
5.000000e-02
5.999999e-02
6.999999e-02
7.999999e-02
8.999999e-02
9.999999e-02
1.100000e-01
1.200000e-01
1.300000e-01
1.400000e-01
1.500000e-01
1.600000e-01
1.700000e-01
1.800000e-01
1.900000e-01
2.000000e-01
2.100000e-01
2.200000e-01
2.300000e-01
2.400000e-01
2.500000e-01
2.600000e-01
2.700000e-01
2.800000e-01
2.900000e-01
3.000000e-01
3.100000e-01
3.200000e-01
3.300000e-01
3.399999e-01
3.499999e-01
3.599999e-01
3.699999e-01
3.799999e-01
3.899999e-01
3.999999e-01
4.099999e-01
4.199999e-01
4.299999e-01
4.399998e-01
4.499998e-01
4.599998e-01
4.699998e-01
4.799998e-01
4.899998e-01
4.999998e-01
5.099998e-01
5.199998e-01
5.299998e-01
5.399998e-01
5.499998e-01
5.599998e-01
5.699998e-01
5.799997e-01
5.899997e-01
5.999997e-01
6.099997e-01
6.199997e-01
6.299997e-01
6.399997e-01
6.499997e-01
6.599997e-01
6.699997e-01
6.799996e-01
6.899996e-01
6.999996e-01
7.099996e-01
7.199996e-01
7.299996e-01
7.399996e-01
7.499996e-01
7.599996e-01
7.699996e-01
7.799996e-01
7.899995e-01
7.999995e-01
8.099995e-01
8.199995e-01
8.299995e-01
8.399995e-01
8.499995e-01
8.599995e-01
8.699995e-01
8.799995e-01
8.899994e-01
8.999994e-01
9.099994e-01
9.199994e-01
9.299994e-01
9.399994e-01
9.499994e-01
9.599994e-01
9.699994e-01
9.799994e-01
9.899994e-01
9.999993e-01

まず、指数の値に注目してみましょう。最初、しばらくの間は -02 ですが、0.1 に到達してからは -01 に切り替わっています。これは、仮数の最上位桁の数字が「1 <= x <= 基数-1」になるような調整を行うからです。1.000000e-020.01 のこと。1.100000e-010.11 のことです。

もう1つ注目すべきなのは、仮数にきれいに 0 が並ばないタイミングがあることです。これは一見すると、0 が並んでいるところでは誤差が出ていなくて、0 が並んでいないところでは誤差が出ているように見えますが、そうではありません。

そもそも、0.01 という 10進数は、2進数では正確に表現できません。0.00000010100011… といったように、どこまでも続いてしまいます。float型の変数 n には、その精度分しか記憶できないので、表現しきれない部分は丸められます。ですから、先ほどの出力結果できれいな数になっているところは、丸めた結果だから、きれいに見えているだけです。

このことは、printf関数の %e 変換指定子に、精度の指定を与えて、出力する桁数を増やしてやるとはっきり分かります。

#include <stdio.h>

int main(void)
{
    float n = 0.0f;

    for (int i = 0; i < 100; ++i) {
        n += 0.01f;
        printf("%.30e\n", n);  // 小数点以下 30桁まで出力
    }
}

実行結果:

9.999999776482582092285156250000e-03
1.999999955296516418457031250000e-02
2.999999932944774627685546875000e-02
3.999999910593032836914062500000e-02
4.999999701976776123046875000000e-02
5.999999493360519409179687500000e-02
6.999999284744262695312500000000e-02
7.999999076128005981445312500000e-02
8.999998867511749267578125000000e-02
9.999998658895492553710937500000e-02
1.099999845027923583984375000000e-01
1.199999824166297912597656250000e-01
1.299999803304672241210937500000e-01
1.399999856948852539062500000000e-01
1.499999910593032836914062500000e-01
1.599999964237213134765625000000e-01
1.700000017881393432617187500000e-01
1.800000071525573730468750000000e-01
1.900000125169754028320312500000e-01
2.000000178813934326171875000000e-01
2.100000232458114624023437500000e-01
2.200000286102294921875000000000e-01
2.300000339746475219726562500000e-01
2.400000393390655517578125000000e-01
2.500000298023223876953125000000e-01
2.600000202655792236328125000000e-01
2.700000107288360595703125000000e-01
2.800000011920928955078125000000e-01
2.899999916553497314453125000000e-01
2.999999821186065673828125000000e-01
3.099999725818634033203125000000e-01
3.199999630451202392578125000000e-01
3.299999535083770751953125000000e-01
3.399999439716339111328125000000e-01
3.499999344348907470703125000000e-01
3.599999248981475830078125000000e-01
3.699999153614044189453125000000e-01
3.799999058246612548828125000000e-01
3.899998962879180908203125000000e-01
3.999998867511749267578125000000e-01
4.099998772144317626953125000000e-01
4.199998676776885986328125000000e-01
4.299998581409454345703125000000e-01
4.399998486042022705078125000000e-01
4.499998390674591064453125000000e-01
4.599998295307159423828125000000e-01
4.699998199939727783203125000000e-01
4.799998104572296142578125000000e-01
4.899998009204864501953125000000e-01
4.999997913837432861328125000000e-01
5.099998116493225097656250000000e-01
5.199998021125793457031250000000e-01
5.299997925758361816406250000000e-01
5.399997830390930175781250000000e-01
5.499997735023498535156250000000e-01
5.599997639656066894531250000000e-01
5.699997544288635253906250000000e-01
5.799997448921203613281250000000e-01
5.899997353553771972656250000000e-01
5.999997258186340332031250000000e-01
6.099997162818908691406250000000e-01
6.199997067451477050781250000000e-01
6.299996972084045410156250000000e-01
6.399996876716613769531250000000e-01
6.499996781349182128906250000000e-01
6.599996685981750488281250000000e-01
6.699996590614318847656250000000e-01
6.799996495246887207031250000000e-01
6.899996399879455566406250000000e-01
6.999996304512023925781250000000e-01
7.099996209144592285156250000000e-01
7.199996113777160644531250000000e-01
7.299996018409729003906250000000e-01
7.399995923042297363281250000000e-01
7.499995827674865722656250000000e-01
7.599995732307434082031250000000e-01
7.699995636940002441406250000000e-01
7.799995541572570800781250000000e-01
7.899995446205139160156250000000e-01
7.999995350837707519531250000000e-01
8.099995255470275878906250000000e-01
8.199995160102844238281250000000e-01
8.299995064735412597656250000000e-01
8.399994969367980957031250000000e-01
8.499994874000549316406250000000e-01
8.599994778633117675781250000000e-01
8.699994683265686035156250000000e-01
8.799994587898254394531250000000e-01
8.899994492530822753906250000000e-01
8.999994397163391113281250000000e-01
9.099994301795959472656250000000e-01
9.199994206428527832031250000000e-01
9.299994111061096191406250000000e-01
9.399994015693664550781250000000e-01
9.499993920326232910156250000000e-01
9.599993824958801269531250000000e-01
9.699993729591369628906250000000e-01
9.799993634223937988281250000000e-01
9.899993538856506347656250000000e-01
9.999993443489074707031250000000e-01

どこの時点においても、0.10 だとか 0.50 というような、きれいにそろった数はないことが分かります。

最初に見た方の実行結果では、小数点以下7桁目のところを丸めた結果が出力されています。この実行環境では、丸め方向が「一番近い値に丸める」なので(本編参照)、以下のような丸めが起こります。

9.9999997 76482582092285156250000e-03  --> 1.000000e-02 (桁が増えるので、指数が 1減る)
1.9999999 55296516418457031250000e-02  --> 2.000000e-02
2.9999999 32944774627685546875000e-02  --> 3.000000e-02
3.9999999 10593032836914062500000e-02  --> 4.000000e-02
4.9999997 01976776123046875000000e-02  --> 5.000000e-02
5.9999994 93360519409179687500000e-02  --> 5.999999e-02
6.9999992 84744262695312500000000e-02  --> 6.999999e-02


参考リンク


更新履歴

’2018/6/14 練習問題を変更。

’2018/6/5 新規作成。余分なものを第19章へ移動させて、内容を一新。



第20章のメインページへ

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る