Programming Place Plus トップページ – C言語編
以下は目次です。
浮動小数点数を扱える型には、float型、double型、long double型の 3つがあります。
float 変数名;
double 変数名;
long double 変数名;
これら3つの型をまとめて、浮動小数点型といいます。
浮動小数点型の具体的な大きさに関して、標準規格では何も既定していません。ただし、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型を使うかどうかは、処理系の事情をよく確認して判断してください。たとえば、Visual Studio の long double型は double型と同じなので、使い分ける意味がありません。
printf関数や scanf関数の変換指定子は、float型は “%f”、double型は “%lf”、long double型は “%Lf” です。
C99規格より前の時代は、printf関数で double型を扱うとき “%f” を指定しなければなりません。一方、scanf関数は必ず “%lf” を指定しなければならず、この不一致が混乱の元になっていました。C99規格になって、printf関数の “%lf” と “%f” が同じ意味になるように修正が加えられました。
#include <stdio.h>
int main(void)
{
float f;
double d;
long double ld;
( "%f", &f ); // float型
scanf( "%lf", &d ); // double型
scanf( "%Lf", &ld ); // long double型
scanf
( "%f\n", f ); // float型
printf( "%lf\n", d ); // double型 ("%f" を使ってもいい)
printf( "%Lf\n", ld ); // long double型
printf}
実行結果:
123.45 <-- 入力内容
123.45 <-- 入力内容
123.45 <-- 入力内容
123.449997
123.450000
123.450000
また、printf関数では、“%.4f” のような形で、出力する小数点以下の桁数を指定できます。double型なら “%.4lf”、long double型なら “%.4Lf” のようになります。
#include <stdio.h>
int main(void)
{
( "%f\n", 1.234567 );
printf( "%.4f\n", 1.234567 );
printf( "%.10f\n", 1.234567 );
printf}
実行結果:
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 があると、その分だけ使える桁数が減ってしまうため、細かい数の表現力が低下してしまいます。
仮数も指数も、使える桁数が限定されています。これは単純な話で、コンピュータで無限個の数値を表現することなどできないからというだけのことです。しかし、浮動小数点数では、仮数と指数を組み合わせるという方法を使って、非常に広範囲の値を表現できるようにしています。
仮数と指数を使ったこの仕組みによって、絶対値が非常に大きい値が表現できます。たとえば、Visual Studio の 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桁しかないとして、どのような丸めが行われるかを示しています。
どの方法が使われるかは実行環境によって異なります。どの方法を使っているか調べる方法を後で取り上げます。
また、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;
( "%e\n", d1 );
printf( "%e\n", d2 );
printf( "%e\n", d3 );
printf}
実行結果:
3.141592e+000
3.141592e+001
3.141592e-001
“%.4e” のようにして、出力する小数点以下の桁数を指定できます。
#include <stdio.h>
int main(void)
{
( "%e\n", 3.141592 );
printf( "%.4e\n", 3.141592 );
printf( "%.10e\n", 3.141592 );
printf}
実行結果:
3.141592e+00
3.1416e+00
3.1415920000e+00
浮動小数点定数を 16進数で表記できます。ただし、その際には科学的記数法を使わなければなりません。
#include <stdio.h>
int main(void)
{
double d = 0x89ab.cdefP2;
( "%a\n", d );
printf( "%f\n", d );
printf}
実行結果:
0x8.9abcdefp+14
140975.217712
16進数での科学的記数法では、先頭に 16進数を表す「0x」を付ける他、「e」を「p (またはP)」に変更します。
「e」が「p」に代わるのは、「e」が 16進数で使うアルファベットに含まていて、区別が付かないからです。
なお、指数部については 10進数で表記しなければなりません。
printf関数や scanf関数に指定する変換指定子は、“%a” または “%A” です。double型なら “%la” や “%lA”、long double型なら “%La” や “%LA” とします。
“%a” と “%A” の違いは、アルファベットの部分が小文字になるか大文字になるかです。
<float.h> を #include して、浮動小数点型に関するさまざまな情報を得られます。
次のプログラムを実行すると、浮動小数点型の限界値(最小値と最大値)が分かります。出力結果は、処理系によって異なります。
#include <stdio.h>
#include <float.h>
int main(void)
{
( " 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 );
printf}
実行結果:
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_MIN、DBL_MIN、LDBL_MIN で、正確に表現できる一番小さい正の数を得られます。
#include <stdio.h>
#include <float.h>
int main(void)
{
( "%e\n", FLT_MIN );
printf( "%e\n", DBL_MIN );
printf( "%Le\n", LDBL_MIN );
printf}
実行結果:
1.175494e-38
2.225074e-308
2.225074e-308
実行結果を見ると分かるように、数そのものは正の数で、指数は大きな負の数になっています。つまり、とても小さい正の数です。この値よりもさらに細かい数は、その型で正確に表現できず、必ず丸めが起こります。
丸め方向は、FLT_ROUNDS で調べられます。ここまでに取り上げてきたものと違って、型による区別はありません。
#include <stdio.h>
#include <float.h>
int main(void)
{
( "%d\n", FLT_ROUNDS );
printf}
実行結果:
1
FLT_ROUNDS で得られる値は以下のいずれか、あるいはこれら以外の処理系定義の値です。
値 意 | 味 |
---|---|
0 | 切り捨てる |
1 | 一番近い値に丸める |
2 | 大きい数になる方向へ丸める |
3 | 小さい数になる方向へ丸める |
-1 | 不確定 |
問題① 次のプログラムを、出力される結果が 1.0 になるように、誤差を考慮した作りに修正してください。
#include <stdio.h>
int main(void)
{
float n;
= 0.0;
n for( int i = 0; i < 100; ++i ){
+= 0.01f;
n }
( "%f\n", n );
printf}
問題② 問題①のプログラムにおいて、変数 n の値がどのように変化しているかを調べて、何が起きているか説明してください。
return 0;
を削除(C言語編全体でのコードの統一)≪さらに古い更新履歴を展開する≫
Programming Place Plus のトップページへ