C言語編 第20章 浮動小数点型

先頭へ戻る

この章の概要

この章の概要です。


浮動小数点型

浮動小数点数を扱える型には、float型double型long double型の 3つがあります。

float 変数名;
double 変数名;
long double 変数名;

これら3つの型をまとめて、浮動小数点型といいます。

浮動小数点型の具体的な大きさに関しては、何も既定されていません。整数型と違って最小の大きさも、ほかの型との兼ね合いも決まっていません。とはいえ多くのコンパイラで、float型が 4バイト、double型が 8バイトです。long double型は、VisualStudio では 8バイト、clang では 16バイトになっています。

float型で表現できる範囲は、必ず double型で表現でき、double型で表現できる範囲は、必ず long double型で表現できることは保証されています。

なお、整数型と異なり、浮動小数点数は常に符号付きです。signed や unsigned を付加することはできません。

float型の定数には、末尾に「F」か「f」を付け、long double型の場合は「L」か「l」を付けます。これらは浮動小数点接尾語と呼び、浮動小数点接尾語を付けない場合には、double型として扱われます

0.1;    /* double型 */
0.1F;   /* float型 */
0.1L;   /* long double型 */

long int型のときと同様、「l」を使うのは見間違いやすいので、「L」を使うことを勧めます。

使い分けの方針としては、基本的に double型を使うのが良いです。メモリ使用量を減らす意味で float型を使うことは考えられますが、表現力が落ちることに注意が必要です。この点は後で取り上げます

実行速度の向上を目当てに float型を使うことを考えるのなら、必ず実測して確認してください。前章の整数型の話の中でも取り上げたように、小さい型だから速いという考えは確実なものではありません。

long double型を使うかどうかは、使用しているコンパイラでの事情をよく確認した方が良いです。例えば、VisualStudio では double型と同じなので、使い分ける意味がありません。

浮動小数点数の入出力

printf関数scanf関数の変換指定ですが、両者で指定の仕方が異なる部分があることに注意が必要です。

printf関数で、float型や double型の値を使う場合、変換指定子に "%f" を使います。long double型の場合は、変換修飾子 "L" を使用して "%Lf" とします。

printf( "%f\n", 123.45f );    /* float型 */
printf( "%f\n", 123.45 );     /* double型 */
printf( "%Lf\n", 123.45L );   /* long double型 */

一方、scanf関数では、単に "%f" とすると、float型を扱うことを意味します。double型を扱うには "%lf"、long double型には "%Lf" としなければなりません。double型の指定方法が、printf関数と異なることに注意が必要です。

scanf( "%f", &f );     /* float型 */
scanf( "%lf", &d );    /* double型 */
scanf( "%Lf", &ld );   /* long double型 */

C99 (printf関数での "%lf"変換指定について)

C99 以降では、printf関数で double型の値を出力するために、"%lf"変換指定を使用できます。これまで通り、"%f" を使うことも可能で、意味は同じです。

#include <stdio.h>

int main(void)
{
    printf( "%f\n", 123.45 );
    printf( "%lf\n", 123.45 );

    return 0;
}

実行結果:

123.450000
123.450000

また、printf関数では、"%.4f" のような形で、出力する小数点以下の桁数を指定できます

#include <stdio.h>

int main(void)
{
    printf( "%f\n", 1.234567 );
    printf( "%.4f\n", 1.234567 );
    printf( "%.10f\n", 1.234567 );

    return 0;
}

実行結果:

1.234567
1.2346
1.2345670000

2つ目の出力結果のように途中の桁で出力が打ち切られる場合、切り捨てなどの処置がなされます(丸めといいます。後述します)。

浮動小数点形式

そもそも、浮動小数点数とはどういう数値なのでしょうか。それなりに難しい話になりますが、必要最小限の部分にだけ触れてみることにします。

浮動小数点数がどのような形で表現されているかについて、厳密な定めはありませんが、大体の考え方を表すことはできます。例えば、次のような形で表現できます。

符号 整数部 小数点 小数部 e 指数

これは考え方を表したものです。実際にメモリ上などで浮動小数点数が表現されるときには、これとは違った形になりますが、構成要素は同じです。多くの処理系では、IEEE 754 という規格に基づいたものになっているはずです。

「符号」は、+ か - のことで、その浮動小数点数が正の数なのか、負の数なのかを示しています。指定しなければ正の数です。

「e」は間を区切っている記号で、それ以上の意味はありません。

この形に従うと、例えば「+12.345e-1」だとか「-0.001e3」といったように書き表せます。このような表記方法は、実際にC言語のソースコード内で使うことも可能です。これは後で触れます

「整数部 小数点 小数部」という固まりと、「指数」の部分が最大のポイントです。まず、「整数部 小数点 小数部」の部分は、合わせて仮数と呼びます。つまり、次のように書き直せます。

符号 仮数 e 指数

書き表された浮動小数点数の実際の数値は、「仮数×基数指数」であり、その符号が「符号」のところで示されています。また、「指数」の部分にも符号が付きますが、これは例えば、2乗なのか -2乗なのかを区別するために使われるものです。

例えば、「+12.345e-1」という浮動小数点数は、「+12.345 * 10-1」であることを意味しています。したがって「+1.2345」のことです。同様に「-0.001e3」なら、「-0.001 * 103」なので「-1.0」のことです。

このような仕組みであるため、同じ数を表現する浮動小数点数のパターンが複数あり得ることに注意して下さい。例えば「1.234e0」「0.1234e1」「123.4e-2」はいずれも同じ値を表しています。

仮数はいわば、表現したい値のベースとなる数です。先ほどの例では仮数を適当に決めましたが、実際には、仮数の最上位桁の数字が「1 <= x <= 基数-1」になるような調整を行います

これは、表現したい値が 0 の場合には不可能です。仕方がないので素直に 0 としておきます。

どうやって調整するかといえば、仮数全体をルールに合った状態になるまで1桁ずつずらしていきます。それと同時に、指数を1ずつ減少させます。10進数でいえば、仮数全体が1桁上位側へずれると 10倍されたことになるので、指数を1減らせば帳尻が合います。このような調整を、正規化といいます。

正規化を行う理由は、高い精度を維持するためです。仮数の上位桁に 0 があると、その分だけ使える桁数が減ってしまうため、細かい数の表現力が低下してしまいます。

仮数も指数も、使える桁数が限定されています。これは単純な話で、コンピュータで無限個の数値を表現することなどできないからというだけのことです。しかし、浮動小数点数では、仮数と指数を組み合わせるという方法を使って、非常に広範囲の値を表現できるようにしています。

仮数と指数を使ったこの仕組みによって、絶対値が非常に大きい値が表現できます。例えば、VisualStudio の float型が扱える最大値は「3.402823466e+38」です。これは 3.402… という仮数を 1038倍した数です。大きさが同じ 32ビットの整数型で表現できる最大値が、4.2…×109 でしかないことを考えると、まるで表現できる範囲が違うことが分かります。

浮動小数点数で表現できる範囲は非常に広いものの、正確に表現できる値はとても少ないということに注意して下さい。無限に存在するはずの実数を、有限の表現力で表現しようとしているので、当然すべての数を正確に表現できる訳ではないのです。

浮動小数点数の表現の細かさは、仮数に何桁割り当てられるかによって決まります。この桁数は、有効桁精度と呼ばれます。

多くの処理系では、float型の仮数の有効桁は、double型の半分以下しかありません。そのため float型で正確に表現できる値の個数は、double型に比べて非常に少なくなります。正確さが必要なら double型を選んだ方が良いということになりますが、だからといって、double型でもすべての数を正確に表現することはできません(繰り返しますが、実数は無限にありますが、浮動小数点数の表現力は有限です)。

IEEE 754 の単精度(binary32)、倍精度(binary64) に従っているとすると、float型の仮数は 24ビット、double型の仮数は 53ビットあります。

有効桁と有効数字

有効桁を正しくカウントする方法は知っておいた方がいいでしょう。そのためには、有効数字という考え方が必要です。ここで有効数字とは、ある数値を表現するために、その数字の存在が必要不可欠なものかどうかということです。

「1.234」の場合、これを構成するすべての数字 (1、2、3、4) には意味があります。どれが欠けても「1.234」にはなりません。そのため、有効数字が 4つあり、有効桁は 4桁です。

「1.23400」とか「1.0」のように、小数点以下にある 0 も有効数字です。これは値の細かさを表すために必要不可欠な 0 です。「1.23400」は有効桁 6桁、「1.0」は 2桁です。

「0.1234」とか「0.01234」のように、上位に 0 が並ぶ場合、その 0 は有効数字ではありません。従って、いずれも有効桁は 4桁です。これは、指数を使って上位の 0 は消せるからと考えると分かりやすいでしょう。例えば「0.1234」は「1.234e-1」ですし、「0.01234」は「1.234e-2」です。いずれも仮数は「1.234」であり、4桁の有効桁で表現できます。

上位に並ぶ 0 が有効数字でないからといって、その数字が必要ないということではありません。少なくとも、その位置に桁があることを示す意味を持っています。

丸め

値が正確に表現できない場合は、その数に近くて正確に表現可能な値を使って、近似的に表現します。このような操作を丸めといいます。

丸めは、実行時に自動的に行われます。そのため、知らないうちに値は正確さを失っている恐れがあります。

丸めの方法は幾つかありますが、代表的なものは以下の4つです。括弧内は、有効桁が 4桁しかないとして、どのような丸めが行われるかを示しています。

どの方法が使われるかは実行環境によって異なります。どの方法を使っているか調べる方法を後で取り上げます

C99 では、fesetround関数の効果によって、丸め方向の設定も変更されます。

誤差

丸めが起こった場合、本来の値を正確に表現していないので、誤差が生まれていることになります。例えば、「1.2345」を「1.235」に丸めたのなら、「0.0005」の誤差が生まれています。このように、丸めによって起こる誤差を、丸め誤差といいます。

誤差は他の場面でも生まれることがあります。ここでは特に代表的な2つのケースを取り上げます。

情報落ち

有効桁が 4桁だとして、「1.234 + 0.0001」という計算を考えてみます。

この計算を普通に行えば「1.2341」になります。しかし、有効桁は 4桁しかないので、これは表現できません。丸めによって、「1.234」や「1.235」といった近似値に直されてしまいます。

結果が「1.234」の方になったとしましょう。期待される「1.2341」という結果と比べると、「0.0001」の誤差が生まれています。

この誤差は取るに足らないものかもしれません。実際、有効桁が 4桁しかないのであれば、5桁目以降にあたる部分が無視されることは当然と捉える見方もあります。しかし、このような小さな数の加算が繰り返されるとしたらどうでしょうか?

例えば、「+ 0.0001」を 100回繰り返したとしても、結果はやはり「1.234」のままです。合計で「0.01」加算されるはずなので、本来なら「1.244」とならなければなりませんから、誤差は「0.01」です。誤差が 100倍に広がってしまいました。1回分の誤差は無視できたとしても、100回、1000回と誤差が蓄積していくと、無視できなくなるかもしれません。

この誤差の原因は、絶対値の差が大きい数同士で加算している点にあります。有効桁が、絶対値が大きい方の数を表現するために消費されてしまいます。「1.234」と「0.0001」の場合、「1.234」という数が有効桁 4桁すべてを使い尽くしてしまいます。ここへ「0.0001」を加算しようとしても、新たな桁(小数点以下 4桁目のところ)を表現できる枠がもうありません。

こうして、絶対値が小さい方の数(あるいはその一部)が失われてしまい、それが誤差となります。この現象は、情報落ちと呼ばれます。

情報落ちを緩和するには、加算する数同士の絶対値の差をできるだけ小さくすることです。例えば「0.0001」を 100回加算するのではなく、「0.0001」を 100倍して「0.01」という数を作り、これを 1回だけ加算します。こうすると、「1.234 + 0.01」という計算になりますから、想定通りの「1.235」という結果が得られます。

桁落ち

「1.234567 - 1.233333」という計算を考えてみます。

それぞれの数の有効桁は 7桁あります。しかし、ここで使われている浮動小数点形式では有効桁数が 4桁しかないとすると、「1.234 - 1.233」のような計算が行われることになります。この時点で丸め誤差を含んでいますが、それだけではなく、得られる結果にも問題があります。

「1.234 - 1.233」の結果は「0.001」です。有効桁 4桁同士で計算を行った結果、有効桁が 1桁の値になっています。これが桁落ちという現象です。

別に問題がないことのように思えますが、本来、この浮動小数点形式の有効桁数は 4桁であることがポイントになります。3桁分の不足を補うために、小数点以下の最下位の部分に 0 を補ってしまうのです。そのため、「0.001」は「0.001000」となります(小数点以下の 0 は有効数字であることを思い出して下さい)。

しかし、そもそも元の計算式は「1.234567 - 1.233333」だったのです。3桁の不足が生まれたのなら、そこには「0.000234」が入るべきなのであって、0 で埋めるべきではないはずです。これが桁落ちによって起こる誤差です。

桁落ちが起こる原因となるのは、近い数による減算です。結果の上位桁に 0 が並んでしまうことによって、有効桁が減ってしまいます(上位桁に並ぶ 0 は有効数字ではない)。

桁落ちを緩和するには、計算の順序を変えるなどして、近い数による減算を回避するしかありません。

誤差が生まれるパターンは幾つかありますが、ともかく、誤差があり得るということを深く意識して下さい。プログラムの内容次第では、多少の誤差は無視して構わないかもしれません。無視ができないのなら、できるだけ避けるように計算の仕方などを工夫する必要があります。あるいは、float型の代わりに、double型や long double型を使うことで、有効桁数が増えて、誤差を小さくできるかも知れませんが、誤差が無くなるとは限りません。

誤差は出てしまうものだと思っておくことも大切です。例えば、2つの浮動小数点数の一致を調べるために等価演算子を使うと、わずかな誤差によって、期待していなかった結果を得てしまう可能性があります。a という浮動小数点数に「0.01」を 10回加算すれば「a + 0.1」とイコールになることを期待しますが、そうはならないかもしれません。

浮動小数点数を比較する際には、誤差を考慮して、許容範囲を持たせた比較を行うと良いです。例えば、「a == b」とするのではなく、以下のような形を取ればよいです。

if( (a >= b - x) && (a <= b + x) ){
    /* 一致したとみなす */
}

x の値をうまく調整して、多少の誤差なら一致しているとみなすようにします。


科学的記数法

ソースコード上でも「符号 仮数 e 指数」の形で表記できます。この方法は、科学的記数法と呼ばれることがあります。浮動小数点接尾語も付けられます。

0.3141592e1;    /* double型 */
0.3141592e1f;   /* float型 */
0.3141592e1L;   /* long double型 */

また、printf関数の変換指定子 "%e" を使うと、科学的記数法で出力できます(long double型のときは "%Le" とします)。実行結果が、科学的記数法で表記した浮動小数点数です。

#include <stdio.h>

int main(void)
{
    double d1 = 3.141592;
    double d2 = 31.41592;
    double d3 = 0.3141592;

    printf( "%e\n", d1 );
    printf( "%e\n", d2 );
    printf( "%e\n", d3 );

    return 0;
}

実行結果:

3.141592e+000
3.141592e+001
3.141592e-001

"%.4e" のようにして、出力する小数点以下の桁数を指定することもできます。

#include <stdio.h>

int main(void)
{
    printf( "%e\n", 3.141592 );
    printf( "%.4e\n", 3.141592 );
    printf( "%.10e\n", 3.141592 );

    return 0;
}

実行結果:

3.141592e+00
3.1416e+00
3.1415920000e+00

C99 (16進数表記の浮動小数点定数)

C99 では、浮動小数点定数を 16進数で表記できるようになりました。ただし、その際には科学的記数法を使わなければなりません。

#include <stdio.h>

int main(void)
{
    double d = 0x89ab.cdefP2;

    printf( "%a\n", d );
    printf( "%f\n", d );

    return 0;
}

実行結果:

0x8.9abcdefp+14
140975.217712

16進数での科学的記数法では、先頭に 16進数を表す「0x」を付ける他、「e」を「p (またはP)」に変更します(「e」は 16進数の文字に含まているので区別が付かないためです)。

なお、指数部については 10進数で表記しなければなりません

printf関数や scanf関数に指定する変換指定子は、"%a" または "%A" です。 前者と後者の違いは、「p」の部分が小文字になるか大文字になるかだけです。

この記法は、VisualStudio 2015/2017 では使用できません。

<float.h>

<float.h> を #include して、浮動小数点型に関する様々な情報を得られます。

限界値

次のプログラムを実行すると、浮動小数点型の限界値(最小値と最大値)が分かります。出力結果は、使用しているコンパイラによって異なります。

#include <stdio.h>
#include <float.h>

int main(void)
{
    printf( "      float型の最小値は %e、最大値は %e\n", -FLT_MAX, FLT_MAX );
    printf( "     double型の最小値は %e、最大値は %e\n", -DBL_MAX, DBL_MAX );
    printf( "long double型の最小値は %Le、最大値は %Le\n", -LDBL_MAX, LDBL_MAX );

    return 0;
}

実行結果:

      float型の最小値は -3.402823e+038、最大値は 3.402823e+038
     double型の最小値は -1.797693e+308、最大値は 1.797693e+308
long double型の最小値は -1.797693e+308、最大値は 1.797693e+308

例えば、FLT_MAX は、float型で表現できる正の最大値を表しています。

注意しなければならないのが最小値の調べ方です。最小値を得る正しい方法は、***_MAX で得られる値にマイナスの符号を付けることです。整数型のときのように、***_MIN を調べることを考えがちですが、それは間違っています。***_MIN もありますが、意味が違うので注意が必要です。

表現可能な最小の正の数

FLT_MINDBL_MINLDBL_MIN で、正確に表現できる一番小さい正の数を得られます。

#include <stdio.h>
#include <float.h>

int main(void)
{
    printf( "%e\n", FLT_MIN );
    printf( "%e\n", DBL_MIN );
    printf( "%Le\n", LDBL_MIN );

    return 0;
}

実行結果:

1.175494e-38
2.225074e-308
2.225074e-308

実行結果を見ると分かるように、数そのものは正の数で、指数は大きな負の数になっています。つまり、とても小さい正の数です。この値よりも更に細かい数は、その型で正確に表現することができず、必ず丸めが起こることになります。

有効桁(精度)

有効桁(精度)は、FLT_DIGDBL_DIGLDBL_DIG で調べられます。

#include <stdio.h>
#include <float.h>

int main(void)
{
    printf( "%d\n", FLT_DIG );
    printf( "%d\n", DBL_DIG );
    printf( "%d\n", LDBL_DIG );

    return 0;
}

実行結果:

6
15
15

実行結果は、10進数で表現したときの有効桁数です。

丸め方向

丸め方向は、FLT_ROUNDS で調べられます。ここまでに取り上げてきたものと違って、型による区別はありません。

#include <stdio.h>
#include <float.h>

int main(void)
{
    printf( "%d\n", FLT_ROUNDS );

    return 0;
}

実行結果:

1

FLT_ROUNDS で得られる値は以下のいずれか、あるいはこれら以外の処理系定義の値です。

意味
0 切り捨てる
1 一番近い値に丸める
2 大きい数になる方向へ丸める
3 小さい数になる方向へ丸める
-1 不確定


練習問題

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

#include <stdio.h>

int main(void)
{
    float n;
    int i;

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

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

    return 0;
}

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


解答ページはこちら

参考リンク



更新履歴

'2018/6/14 全体的に構成と内容を修正。
浮動小数点数の入出力」「浮動小数点形式」「丸め」「誤差」「<float.h>」の項を追加。
練習問題を変更。

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



前の章へ(第19章 整数型)

次の章へ(第21章 型変換)

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

Programming Place Plus のトップページへ


はてなブックマーク Pocket に保存 Twitter でツイート Twitter をフォロー
Facebook でシェア Google+ で共有 LINE で送る rss1.0 取得ボタン RSS
管理者情報 プライバシーポリシー