浮動小数点型 | Programming Place Plus C言語編 第20章

トップページC言語編

このページの概要

以下は目次です。


浮動小数点形式

ここまでの章では、数値はほぼ整数に限定して話を進めてきました。一応、第4章で double型という、浮動小数点数 (floating point number) を取り扱う型に触れていますが、実質的に使ったことはありませんでした。この章で浮動小数点数について説明し、以降は整数だけでなく、浮動小数点数も使っていくことにします。

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

浮動小数点数は次のようなかたちで表現される数です1

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

【上級】これは考え方を表したもので、一種のモデルとして示されているものです。実際の表現はこれとは違った形になりえます。多くの処理系では、IEEE 754 という規格に基づいたものになっているはずです。

たとえば +12.345e-1 だとか -0.001e3 といったように書き表せるということで、実際このとおりにC言語のソースコードに記述できます。これは後で触れます

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

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

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

符号 仮数 e 指数

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

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

このような仕組みであるため、同じ数を表現する浮動小数点数のパターンが複数あり得ることに注意してください。たとえば、1.234e00.1234e1123.4e-2 はいずれも同じ値を表しています。

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

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

【上級】どうやって調整するかといえば、仮数全体をルールに合った状態になるまで1桁ずつずらしていきます。それと同時に、指数を1ずつ減少させます。10進数でいえば、仮数全体が1桁上位側へずれると 10倍されたことになるので、指数を1つ減らせば帳尻が合います。このような調整を、正規化 (normalization) といいます。正規化を行う理由は、高い精度を維持するためです。仮数の上位桁に 0 があると、その分だけ使える桁数が減ってしまうため、細かい数の表現力が低下してしまいます。

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

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

浮動小数点数で表現できる範囲は非常に広いものの、正確に表現できる値はとても少ないということに注意してください。無限に存在するはずの実数を、有限の表現力で表現しようとしているので、当然すべての数を正確に表現できるわけではないのです。浮動小数点数の表現の細かさは、仮数に何桁割り当てられるかによって決まります。この桁数を有効桁 (Effective digits) と呼びます。

有効桁と有効数字

有効桁を正しくカウントする方法は知っておいた方がいいでしょう。そのためには、有効数字 (significant digits、significant_figures) という考え方が必要です。

有効数字とは、ある数値を表現するために、その数字の存在が必要不可欠なものかどうかということです。たとえば、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 は有効数字ではありません。これは、指数を使って上位の 0 は消せるからと考えると分かりやすいでしょう。たとえば 0.12341.234e-1 ですし、0.012341.234e-2 です。いずれも仮数は 1.234 であり、4桁の有効桁で表現できます。いずれも有効桁としては 4桁ということになります。

浮動小数点型

浮動小数点数を扱う型は、浮動小数点型 (floating type)と総称されます。浮動小数点型はさらに、実浮動小数点型 (real floating type) と複素数型 (complex type) に分かれますが、後者は特定の分野以外では滅多に使われることがないので、ここでは取り上げないことにします。

複素数型は C99規格で追加されたものであり、それ以前は実浮動小数点型にあたるものしかなかったため、今でも、単に「浮動小数点型」といって、実浮動小数点型のことを指しているケースが多いです。

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

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

実浮動小数点型の大きさに関して、標準規格では何も既定していません。ただし、float型で表現できる値は double型で表現でき、double型で表現できる値は long double型で表現できることは保証されています。2

なお、浮動小数点型はつねに符号付きであり、signed や unsigned を付加できません。

浮動小数点定数

浮動小数点型の定数(浮動小数点定数 (floating constant))には小数点 . が必要です。小数点よりも上位の桁がすべて 0 の場合には .123 のような表記が許され、下位の側の桁がすべて 0 の場合には 123. のような表記が許されます。. は必要なので、0.0 を 0 とすることはできません(これは整数リテラルの 0 です)。また、あとで取り上げる科学的記数法という記述方法もあります。

0.0 を 0 とすると整数リテラルになってしまいますが、整数型から実浮動小数点型へは暗黙の型変換が働くため、double d = 0; のような表示は実際のところ動作します。

浮動小数点定数の型は double型です。ほかの型にしたいときは、サフィックスを付加します。

サフィックス
なし double
f または F float
l または L long double
0.1;    // double型
0.1F;   // float型
0.1L;   // long double型

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

実浮動小数点型の使い分けの方針ですが、基本的には double型を使えば良いです。メモリの使用量を減らす意味で float型を使うことは考えられますが、有効桁が小さくなることに注意が必要です。多くの処理系では、float型の仮数の有効桁は、double型の半分以下しかありません。そのため float型で正確に表現できる値の個数は、double型に比べて非常に少なくなります。正確さが必要なら double型を選んだ方が良いですが、だからといって、double型でもすべての数を正確には表現できません(繰り返しになりますが、実数は無限にあるのに対して、浮動小数点数の表現力は有限です)。

long double型を使うかどうかは、処理系の事情をよく確認して判断してください。たとえば、Visual Studio の long double型は double型と同じなので3、使い分ける意味がありません。

科学的記数法

浮動小数点定数を「符号 仮数 e 指数」の形でも表記できます。この方法は、科学的記数法 (scientific notation) と呼ばれることがあります。型を明示するためのサフィックスを付加することもできます。

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

16進数表記の浮動小数点定数

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

Visual Studio 2015 では対応されていません。2017 から使えるようになっています。

0xab.cdefP2;    // double型
0xab.cdefP2f;   // float型
0xab.cdefP2L;   // long double型

16進数での科学的記数法では、先頭に 16進数を表す 0x を付けるほか、e の部分を p または P に変更します。また、指数部については 10進数で表記しなければなりません

ep に変えるのは、e が 16進数で使うアルファベットに含まていて区別が付かないからです。

浮動小数点数の出力

printf関数を使って浮動小数点数を取り扱うときの変換指定子は、値の表記方法の違いによっていくつか存在します。

変換指定子 意味
%f または %F 1.234 のような一般的な表記
%e または %E 科学的記数法
%g または %G 渡された値に応じて、%f か %e の記法のいずれかを使用
%a または %A 16進数表記の科学的記数法

%g は簡単にいえば、より簡潔になる表記を選んで出力するということです。ただし、末尾の 0 を(さらに小数部がなければ . も)取り除くという仕様があるため4%f%e のいずれの結果とも異なる場合があります。

いずれの場合も、関数に渡す値の型は double型です。long double型の値を渡す場合は L という長さ修飾子を使って、%Lf のように指定します。float型の場合は %f で構いません。また、double型の意味で %lf とすることも許されています。

sscanf関数では、double型の値を受け取る場合は %lf、float型のときは %f のように使い分けなければなりません。この差異は分かりづらいため、C99規格からは printf関数でも、double型の意味で %lf を使えるようになりました5

【上級】float型の値を渡すときに変換指定子を変えなくていいのは、可変個引数の関数に実浮動小数点型の値を渡すとき、暗黙的に double型に変換されるためです(第52章)。

#include <stdio.h>

int main(void)
{
    float f = 123.45f;
    double d = 123.45;
    long double ld = 123.45L;

    printf("%f\n", f);
    printf("%e\n", f);
    printf("%g\n", f);
    printf("%a\n", f);

    printf("%lf\n", d);
    printf("%le\n", d);
    printf("%lg\n", d);
    printf("%la\n", d);

    printf("%Lf\n", ld);
    printf("%Le\n", ld);
    printf("%Lg\n", ld);
    printf("%La\n", ld);
}

実行結果:

123.449997
1.234500e+02
123.45
0x1.edcccc0000000p+6
123.450000
1.234500e+02
123.45
0x1.edccccccccccdp+6
123.450000
1.234500e+02
123.45
0x1.edccccccccccdp+6

%f%e のデフォルトの精度は 6桁であり、小数点以下に 6桁分の表示があります。精度は %.3f とか %.12Lf のように、変換指定内の . のうしろに整数で指定できます。

#include <stdio.h>

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

実行結果:

123.456780
123.4568
123.4567800000

2つ目の出力結果のように、指定した精度が小さくて、途中の桁で打ち切られる場合、切り捨てなどの処置がなされます(後述します)。

浮動小数点数の入力

sscanf関数を使って浮動小数点数を取り扱うときの変換指定子には、printf関数と同様に %f%F%e%E%g%G%a%A があります。しかし sscanf関数の場合はどれを使っても同じです。1.234 のような通常の表記方法でも、科学的記数法でも、16進数による科学的記数法でも取り扱えます6

受け取る値は float型になります。double型にしたければ %lf、long double型にしたければ %Lf を使います。printf関数と違い、%f%lf は意味が異なっており、確実に使い分けなければなりません。

#include <stdio.h>

int main(void)
{
    char input_string[40];

    fgets(input_string, sizeof(input_string), stdin);
    float f;
    sscanf(input_string, "%f", &f);
    printf("%f\n", f);

    fgets(input_string, sizeof(input_string), stdin);
    double d;
    sscanf(input_string, "%lf", &d);
    printf("%lf\n", d);

    fgets(input_string, sizeof(input_string), stdin);
    long double ld;
    sscanf(input_string, "%Lf", &ld);
    printf("%Lf\n", ld);
}

実行結果:

123.456  <-- 入力した内容
123.456001
-0.123e2  <-- 入力した内容
-12.300000
0xabP-1  <-- 入力した内容
85.500000

丸め

値が正確に表現できない場合、その数に近い、正確に表現できる値を使って近似的に表現されます。このような操作を丸め (rounding) といいます。丸めは、実行時に自動的に行われているため、知らないうちに値は正確さを失っている恐れがあります。

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

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

また、fesetround関数を使って、実行環境がサポートしている範囲で、丸め方向の設定を変更できます。

誤差

丸めが起こった場合、本来の値を正確に表現していないので、誤差 (error) が生まれています。たとえば、1.23451.235 に丸めたのなら、0.0005 の誤差が生まれています。このように、丸めによって起こる誤差を、丸め誤差 (rounding error) といいます。

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

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

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

if (fabs(a - b) <= x) {  // 誤差が x より小さいなら、一致していることにする
}

fabs関数は、実引数の値の絶対値を返す関数で、#include <math.h> をすると使えるようになります。fabs関数の仮引数と戻り値は double型ですが、float型版の fabsf関数、long double型版の fabsl関数もあります。

また、標準ライブラリには、上記コードの x として使えるマクロが定義されており、#include <float.h> をすると使えるようになります。

マクロ
FLT_EPSILON float
DBL_EPSILON double
LDBL_EPSILON long double

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

情報落ち

有効桁が 4桁だとして、1.234 + 0.0001 という計算を考えてみます。この計算を普通に行えば、結果は 1.2341 です。しかし有効桁が 4桁しかないので、これは表現できません。丸めによって、1.2341.235 といった近似値になってしまいます。

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

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

この誤差の原因は、絶対値の差が大きい数同士で加算している点にあります。有効桁が、絶対値が大きい方の数を表現するために消費されてしまいます。1.2340.0001 の場合、1.234 という数が有効桁 4桁すべてを使い尽くしてしまいます。ここに 0.0001 を加算しようとしても、新たな桁(小数点以下 4桁目のところ)を表現できる枠がもうありません。こうして、絶対値が小さい方の数(あるいはその一部)が失われてしまい、それが誤差になります。この現象は、情報落ち (loss of trailing digits) と呼ばれます。

情報落ちを緩和するには、加算する数同士の絶対値の差をできるだけ小さくすることです。たとえば、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桁に減っています。これが桁落ち (loss of significance) という現象です。

別に問題がないことのようにもみえますが、本来、この浮動小数点形式の有効桁数は 4桁であることがポイントです。3桁分の不足を補うために、小数点以下の最下位の部分に 0 を補ってしまうのです。そのため、0.0010.001000 になります(小数点以下の 0 は有効数字であることを思い出してください)。しかし、元の計算式は 1.234567 - 1.233333 だったので、3桁の空きが生まれたのなら、そこには 0.000234 が入ってほしいのであって、0 で埋められたくはないはずです。これが桁落ちによって起こる誤差です。

桁落ちが起こる原因となるのは、近い数による減算です。結果の上位桁に 0 が並んでしまうことによって、有効桁が減ってしまいます(上位桁に並ぶ 0 は有効数字ではない)。桁落ちを緩和するには、計算の順序を変えるなどして、近い数による減算を回避するようにします。

さまざまな情報の取得

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

限界値

次のプログラムを実行すると、それぞれの型の限界値(最小値と最大値)が分かります。出力結果は、処理系によって異なります。

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

int main(void)
{
    printf("float: (MIN)%e  (MAX)%e\n", -FLT_MAX, FLT_MAX);
    printf("double: (MIN)%le  (MAX)%le\n", -DBL_MAX, DBL_MAX);
    printf("long double: (MIN)%Le  (MAX)%Le\n", -LDBL_MAX, LDBL_MAX);
}

実行結果:

float: (MIN)-3.402823e+38  (MAX)3.402823e+38
double: (MIN)-1.797693e+308  (MAX)1.797693e+308
long double: (MIN)-1.797693e+308  (MAX)1.797693e+308

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

注意しなければならないのが最小値の調べ方です。最小値を得る正しい方法は、FLT_MAX で得られる値にマイナスの符号を付けることです。整数型の場合の感覚で、FLT_MIN を調べようとしがちですが、それは間違っています。FLT_MIN も存在しますが、意味が違うので注意が必要です(後述します)。

表現できる最小の正の数

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

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

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

実行結果:

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);
}

実行結果:

6
15
15

実行結果は、10進数で表現したときの精度です。

丸め方向

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

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

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

実行結果:

1

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

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


練習問題

問題① 次のプログラムを、出力される結果が 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);
}

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


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



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

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

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

Programming Place Plus のトップページへ



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