閉じる

なぜ0.1+0.2は0.3にならないのか?浮動小数点数の基礎

プログラムで0.1+0.2が0.3にならない、と表示されて戸惑ったことはありませんか。

これはバグではなく、コンピュータが小数を2進数で近似して扱うしくみ(IEEE 754)の性質です。

この記事では、原因と実用的な対処法を初心者向けにやさしく解説します。

「なぜそうなるのか」と「どう避けるか」を順に理解していきます

浮動小数点数(IEEE 754)の基本

小数はビットで近似する

コンピュータは0や1のビットで数を表現します。

小数も例外ではなく、浮動小数点数は「符号」「指数」「仮数(有効桁)」の3つに分けて、実数を近似して表現します

直感的には、10進数でいうところの「1.23 × 10の何乗」を、2進数版の指数表記で持っているイメージです。

イメージ

  • 符号: プラスかマイナスか。
  • 指数: 桁をどのくらい動かすか。
  • 仮数: 重要な桁(有効数字)を保持する部分。

この方式により、非常に大きい数から非常に小さい数まで幅広く扱えますが、仮数に割り当てられたビット数が有限であるため「ぴったり」表せない小数が多くあります

固定ビット数と精度の限界

浮動小数点はビット数が決まっています。

例えば倍精度(64ビット)では、仮数部は52ビットで、約15〜17桁の10進有効桁を表現できます。

これは多くの処理には十分ですが、全ての小数を正確に格納できるわけではありません

表せない値は「最も近い表現可能な値」に丸められます。

  • ビット数が決まっているため、無限に続く小数はどこかで打ち切り(丸め)が必要です。
  • 丸めた誤差が計算の途中や表示で「目に見える」ことがあります

10進小数と2進小数の違い

10進では有限小数でも、2進では有限にならないことがよくあります。

0.5や0.25は2の累乗分の1なので2進で有限ですが、0.1や0.2は2進では無限に続く小数になります

  • 5(10進) = 0.1(2進) → 有限
  • 25(10進) = 0.01(2進) → 有限
  • 1(10進) ≈ 0.0001100110011…(2進) → 無限小数
  • 2(10進) ≈ 0.001100110011…(2進) → 無限小数

つまり、0.1 も 0.2 も内部では「近いけれどズレた値」として格納されます。

倍精度(64ビット)が一般的

現在の多くの言語やランタイムでは、標準の数値型に倍精度(64ビット)を採用しています。

特にJavaScriptのnumber、Pythonのfloat、Goのfloat64(デフォルト推奨)などは64ビット倍精度です

一方で、GPUやメモリ節約が重要な場面では単精度(32ビット)が使われることもあります。

以下は代表的な違いの目安です。

形式ビット数おおよその10進有効桁主な用途
単精度(float)32約6〜7桁グラフィックス、メモリ節約が重要な場面
倍精度(double)64約15〜17桁一般的なアプリケーション、科学技術計算

初心者の方は、まず倍精度前提で理解すると学習しやすいです

なぜ0.1+0.2は0.3にならないのか

0.1は2進数で無限小数になる

すでに見たとおり、0.1や0.2は2進で無限に続く小数なので、有限ビットでは途中で切り上げや切り捨てが必要です。

保存されるのは「最も近い表現可能な値」ですが、そこには微小な誤差が含まれます。

実例(表示)

Python
>>> 0.1 + 0.2
0.30000000000000004
JavaScript
console.log(0.1 + 0.2); // 0.30000000000000004

丸め誤差が加算で表に出る

丸め誤差は単体では目立ちませんが、加減乗除を重ねると誤差が表面化します。

0.1も0.2も「ややズレた値」なので、その和も「ややズレた値」になるのです。

イメージ

  • 1 ≈ 0.10000000000000000555…
  • 2 ≈ 0.20000000000000001110…
  • 和 ≈ 0.30000000000000004440… → 0.30000000000000004 と表示

誤差はとても小さいのですが、表示桁数が多いと見えてしまいます

0.30000000000000004と表示される理由

多くの言語は、数値を文字列に変換する際に「その数を再現できるだけの桁数」を出力しようとします

そのため、内部のわずかなズレが見える形で「0.30000000000000004」と表示されます。

表示の問題に見えますが、根本は内部表現の丸めです。

==比較が失敗する理由

プログラムの等値比較(==など)は、ビットに格納された値が完全に同じかを見ます。

0.3(リテラル)の「最も近い値」と、0.1+0.2の「最も近い値」は、丸めの経路が違うため一致しません。

実例(比較)

0.1 + 0.2 === 0.3 // false
(0.1 + 0.2) == 0.3  # False

数値の等値比較に == を安易に使うのは危険です。

次のセクションで安全な比較方法を説明します。

初心者向けの対処法とベストプラクティス

許容誤差(epsilon)で比較する

最も基本的な方法は、「十分に近ければ同じとみなす」ことです。

差が小さな閾値(イプシロン)以下なら等しいと判断します。

桁スケールが大きく変わる場合は、相対誤差も考慮します。

実例

JavaScript
function isClose(a, b, eps = 1e-12) {
  return Math.abs(a - b) <= eps * Math.max(1, Math.abs(a), Math.abs(b));
}
isClose(0.1 + 0.2, 0.3); // true
Python
import math
math.isclose(0.1 + 0.2, 0.3)  # True (デフォルトの相対/絶対許容誤差)

標準関数がある言語では、それを使うのが安全です。

金額は整数で扱う

金額のように「誤差が許されない」領域では、最小単位で整数として扱うのが基本です。

円ならそのまま整数、ドルならセント(100倍)で整数化します。

金額を浮動小数点で直接計算しないのが鉄則です。

実例

JavaScript
1000円 + 250円
const totalYen = 1000 + 250; // 1250 (整数)
Python
// $10.99 + $0.10
const totalCents = 1099 + 10; // 1109 cents -> $11.09

税計算など丸め規則がある場合は、どのタイミングで四捨五入するか(毎行・小計・総計)を仕様として統一します。

Decimal(任意精度)を使う

金融や高精度が必要な場面では、10進の任意精度(Decimal/BigDecimal)を使います

これは10進の有理数を正確に扱いやすく、0.1のような値を「正確に」表現できます。

実例

Python
from decimal import Decimal, getcontext
getcontext().prec = 28
Decimal("0.1") + Decimal("0.2") == Decimal("0.3")  # True
Java
import java.math.BigDecimal;
new BigDecimal("0.1").add(new BigDecimal("0.2"))
                     .compareTo(new BigDecimal("0.3")) == 0; // true
JavaScript
const Decimal = require('decimal.js');
new Decimal('0.1').plus('0.2').equals(new Decimal('0.3')); // true

文字列から生成するのがポイントです。

0.1のような浮動小数点から直接変換すると誤差を引き継ぎます。

出力は丸めてフォーマットする

画面やレポートに出すときは、必要な小数点以下桁数に丸めてフォーマットします。

内部の誤差が見えないようにしながら、表記を統一できます。

実例

JavaScript
(0.1 + 0.2).toFixed(2); // "0.30"
Python
format(0.1 + 0.2, ".2f")  # '0.30'

丸め方(四捨五入、銀行丸めなど)は要件に合わせて選びます。

テストは近似比較にする

単体テストで浮動小数点を直接 == 比較すると不安定になります。

「近似比較」を使うのが実務的です。

実例

Python
from pytest import approx
assert (0.1 + 0.2) == approx(0.3, rel=1e-12, abs=1e-12)
JavaScript
expect(0.1 + 0.2).toBeCloseTo(0.3, 12);

つまずきやすいポイントと確認方法

足し算の順序で結果がわずかに変わる

浮動小数点では、(a + b) + c と a + (b + c) が異なる結果になる場合があります

これは丸めが各ステップで起きるためです。

特に、非常に大きな数と非常に小さな数の足し算で差が出やすいです。

実例

JavaScript
const a = 1e16, b = 1, c = -1e16;
console.log((a + b) + c); // 0 (bが消える)
console.log(a + (b + c)); // 1

対策として、小さい値同士・大きい値同士から集計する(ペア和)やKahan法などの手法が知られていますが、初心者はまず「順序によって誤差が変わる」点を覚えておくと良いです。

JSONや文字列化で桁が増える

JSONやログに数値を出力すると、内部のわずかなズレが露出して桁が増えて見えることがあります。

これは不具合ではなく、復元可能な表現を優先するための仕様です。

実例

JavaScript
JSON.stringify(0.1 + 0.2); // "0.30000000000000004"
(0.1 + 0.2).toPrecision(12); // "0.300000000000"

画面表示用には toFixed やフォーマット関数を使い、「見せる値」と「内部値」を分けると混乱が減ります。

入力のパースと丸めに注意

文字列から数値に変換する際、parseの時点で浮動小数点にすると誤差が入り得ます

金額や桁が重要な値は、Decimalでパースするか、整数(最小単位)に変換するのが安全です。

また、ロケール(小数点がカンマの地域)にも注意が必要です。

ヒント

  • 文字列 → Decimal/BigDecimal へ直接。
  • 文字列 → 整数(最小単位にスケール) → 計算 → 最後に表示用フォーマット。

言語別の注意点

各言語の標準的な数値型と、おすすめの回避策をざっくりまとめます。

迷ったら「金額は整数かDecimal」「比較は近似」を合言葉にしましょう。

言語標準の実数型金額向け近似比較
JavaScriptnumber(倍精度)整数(セント)または decimal.js 等独自isClose/テストでtoBeCloseTo
Pythonfloat(倍精度)Decimalmath.isclose/pytest approx
Javadouble(倍精度)BigDecimal誤差許容の自前比較
C#double(倍精度)decimal 型誤差許容の自前比較
Gofloat64(推奨)整数スケール/shopspring/decimal 等誤差許容の自前比較
RubyFloat(倍精度)BigDecimal(a – b).abs < eps
SwiftDouble(倍精度)Decimal/NSDecimalNumber(a – b).magnitude < eps

どの言語でも「文字列からDecimal/BigDecimalを生成」するのが重要です。

まとめ

0.1+0.2が0.3にならないのは、コンピュータが小数を2進数で有限ビット近似する(IEEE 754)ためです。

0.1や0.2は2進で無限小数になり、丸め誤差が加算で表面化して「0.30000000000000004」と表示されます。

等値比較(==)が失敗するのも、この微小な差が原因です。

実務では、(1) 許容誤差で比較する、(2) 金額は整数やDecimalで扱う、(3) 表示は丸めてフォーマット、(4) テストは近似比較、といった定番のベストプラクティスで安全に取り扱えます。

これらを押さえておけば、浮動小数点の「不思議」は予測可能な挙動に変わり、安心してプログラミングが進められます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

このサイトでは、プログラミングをこれから学びたい初心者の方に向けて記事を書いています。 基本的な用語や環境構築の手順から、実際に手を動かして学べるサンプルコードまで、わかりやすく整理することを心がけています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!