arrow 若葉プログラミング塾 > 知識の玉手箱 > ワンポイントTips >
数値の扱いに関する諸問題

 型を意識するプログラム言語では、数値的な問題が避けて通れない。

 大きく分けて、これらの諸問題には2通りある。ひとつは、桁落ち、桁あふれなどの格納域サイズと精度に関する問題、 もうひとつは異なる型の混在する式における、暗黙の型変換である。

 これらを正しく理解しておくことで、多くの問題が回避でき、ソースコードの品質は向上する。

 そんなめんどっちいことは考えたくもない。という向きには、読み飛ばしてもらってもかまわない話題なのだが、おそらくデバッグによけいな時間を食うだろう。 その結果「なんだ、そういうことか。もっと早く言ってよ!」となること請け合いなのだ。

格納域サイズに関する問題

(1)桁あふれ(オーバーフロー)

 桁落ちとは、かつてコンピュータがきわめて限られた資源しかもたなかった時代に、大きな問題として注意を要した。 たとえば8ビットの整数型があったとして、そこに入れることが可能な数値範囲は、0〜256(符号なし)もしくは−128〜127(符号あり)になる。 ところがうっかり

val1 = 32;
val2 = 10;
val3 = val1 * val2;

 などという計算を行ったとしよう。なんとval3は64になったりしてしまうのだ。これを桁あふれといい、そのままプログラムを続行するとえらいことである。 なぜこうなるのかは、C言語コースの第五章を見て考えて欲しい。

 これを回避するには、十分な格納域が保証される型を使用するか、その型が存在しなければdoubleやfloatなどの実数型を使用するしかない。

(2)桁落ち

 さて、整数型では不安だと言うので、実数型を使用したとしよう。それはそれで、オーバーフローの可能性はほとんどなくなるが(絶対無いわけではない)、 別の問題が起こることがある。

 当然といえば当然であろう。実数型に問題がなければ、誰もが実数型を使うはずで、整数型なんていらないことになる。 第一の問題は演算速度である。よく知られているように、整数型のほうがダンゼン計算が速い。もともと2進数の世界なので、CPUも2進数を扱うのは得意中の得意である。 整数型はそのままビットシフトすれば掛け算や割り算になるので、瞬きする間に計算してしまうが、実数型の計算はもっと複雑だ(浮動小数点数参照)。

 しかしながら、マシン性能が格段に向上した昨今では、あまり計算速度が問題になることはなく、むしろネットワークトラフィックやハードウェアの制御、メモリ不足などがネックになることが多い。

だからといって整数ですむ計算を、わざわざ実数で行うことはない。遅いより早いほうがいいだけでなく、整数型にはない問題をかかえることになる。

 第二の問題は「桁落ち」や「丸め誤差」、つまり精度の問題である。次のソースコートを実行してみて欲しい。

#include <stdio.h>
void main() {
	float a = 1.0000001, b = 1.0000000;
printf("a =%1.10f\n", a); printf("b =%1.10f\n", b); printf("a-b=%1.10f\n", a - b); }

2つの実数を以下のように決める。
a=1.0000001
b=1.0000000

ここで、2つの実数の差を考えると、

a-b=0.0000001

のはずである。ところが、実行結果は、

a =1.0000001192
b =1.0000000000
a-b=0.0000001192

ここで予想と違ってしまったのはなぜか。

右端に出てくる「1192」という値のうち「192」は、変数 a に入っている値「1.0000001」の、float型の有効桁をこえる部分を示しているからなのだ。

つまり、a=1.0000001000000
ではなくて、a=1.0000001??????
なのである(?は何が入っているかわからない)。

だから、結果(a-b)の0.0000001192の信用できる数字は、一番左の1だけということになる。

本来、floatは、約7桁の精度があるのだが、それが演算によって1桁になってしまったことがわかる。 これを桁落ち(有効桁数の減少)と呼ぶ。

実数を扱う場合、常に必要な精度が保たれているかを意識する必要がある。

異なる型の混在

int a, c;
double b;

a = 10;
b = 3.14;
c = a * b;

 このときcには何が入るか。手計算では31.4であるが、実際には31が入る。なぜならcはint型だからである。

 ではなぜ、c=30ではないのか。整数型と実数型との混在する式では、ある優先順位に従って暗黙の型変換がおこるからである。型変換は型キャストともいい、型キャストには「暗黙の型キャスト」と「明示的な型キャスト」がある。

上記の例では「暗黙の型キャスト」が適用された。どのように変換されたか見てみよう。

int a, c;
double b;

a = 10;
b = 3.14;
c = (int)((double)a * b);

 明示的な型キャストで書くとこうなる。C言語でもJava言語でもC++言語でも、型キャストの記法は同一で、式の前に変換後の型をカッコつきで指定する。 上記の式は、まずaをint->doubleに変換し、そののちa*bを実行し、その結果をdouble->intに変換してcに代入する。というストーリーを示すものだ。

 aはintだが計算前にdoubleに変換される。10.0という実数に化けている。なので、bとの計算は同じ型同士で行える。この時点で計算結果は31.4だから、そのままint型に代入はできない。そこで再度型キャストが勝手に起こる。31.4は切り捨てられ、31という整数にされてしまうのだ。

 型に優先順位があるために、doubleがintに勝つのである。優先順位はこう覚えてよい。

    格納域の大きいほうが強い!!!

整数型同士ではlongはshortより強い。整数と実数では実数が強い。プログラミングの世界では弱肉強食なのだ。 ただし、代入時にはいうまでもなく無条件に左辺が強い。

 言語によっては、暗黙の型キャストを行ってくれず、明示的にキャストしないとコンパイルエラーになるものもある(Javaなど)。 エラーにならなくても、代入時には値が切り捨てられる可能性もあるから、警告メッセージがでるので、大丈夫かどうかはプログラマー自身が判断しなければならない。 そういう意味で、「判断済み」というサインとしてキャストを指定するのが、めんどうでも確実であるといえよう。

数値の扱いに関する諸問題
arrow 若葉プログラミング塾 > 知識の玉手箱 > ワンポイントTips >
KC