Chapter Floating-Point sebelumnya mengajarkan cara menulis literal float dan double; chapter ini membuka tutup mesinnya dan menunjukkan bagaimana angka-angka itu benar-benar tersimpan di memori. Standar IEEE 754 yang dipakai hampir semua CPU modern menyimpan setiap floating-point dalam tiga bagian bit: satu bit sign (tanda), sekelompok bit exponent (eksponen), dan sekelompok bit mantissa (signifikannya) — struktur yang membuat penjumlahan dan perkalian bisa dikerjakan oleh rangkaian digital dengan cepat. Konsekuensi paling penting dari format biner ini: banyak angka desimal sederhana seperti 0,1 tidak punya representasi persis, sehingga nilai yang tersimpan selalu pembulatan terdekat. Kita akan membedah bit 0.1f langsung dari memori, membandingkan presisi float 32-bit dan double 64-bit, lalu mengenal dua nilai khusus — inf dan nan — yang dicetak deterministik saat perhitungan keluar jalur.

Anatomi IEEE 754 #

Untuk float (32 bit), pembagian bitnya seperti tabel berikut; double mengikuti pola yang sama dengan 1 bit sign, 11 bit exponent, dan 52 bit mantissa:

BagianfloatdoubleIsinya
sign1 bit1 bit0 = positif, 1 = negatif
exponent8 bit11 biteksponen basis 2 yang digeser (bias) 127 (float) atau 1023 (double)
mantissa23 bit52 bitdigit biner setelah tanda “1,” yang tersirat

Skema ini meniru notasi ilmiah biner: nilai = (-1)^sign × 1.mantissa × 2^exponent. Angka 0,1 desimal misalnya = 1,6 × 2⁻⁴, dan pecahan biner 0,6 = 0,1001100110011… berulang tanpa henti. Karena mantissa hanya punya 23 bit (float), deret berulang itu dipotong dan dibulatkan — inilah sumber selisih 0,1 yang akan kita buktikan lewat kode.

Membedah Bit 0.1f Langsung dari Memori #

Program berikut menyalin pola bit mentah 0.1f ke integer 32-bit, lalu memecahnya menjadi tiga field IEEE 754:

cpp
#include <bitset>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <limits>

int main() {
    // 1. Bedah bit: bagaimana 0.1f disimpan (IEEE 754 single precision)
    float nilai = 0.1f;
    std::uint32_t bit = 0;
    std::memcpy(&bit, &nilai, sizeof bit);   // salin pola bit mentah

    std::cout << "bit lengkap  : " << std::bitset<32>(bit) << "\n";
    unsigned sign   = bit >> 31;
    unsigned ekspon = (bit >> 23) & 0xFFu;
    unsigned mantis = bit & 0x7FFFFFu;
    std::cout << "sign         : " << sign << "\n";
    std::cout << "exponent     : " << std::bitset<8>(ekspon)
              << " (= " << ekspon << ", bias 127 -> "
              << static_cast<int>(ekspon) - 127 << ")\n";
    std::cout << "mantissa     : " << std::bitset<23>(mantis) << "\n";

    // 2. float vs double: derajat ketidaktepatan yang berbeda untuk 0.1
    std::cout << std::setprecision(std::numeric_limits<float>::max_digits10);
    std::cout << "0.1f         : " << nilai << "\n";
    std::cout << std::setprecision(std::numeric_limits<double>::max_digits10);
    std::cout << "0.1 (double) : " << 0.1 << "\n";

    // 3. Nilai khusus: infinity dan NaN
    double nol = 0.0;
    double inf = 1.0 / nol;          // pembagian dengan nol -> infinity
    std::cout << "1.0/0.0      : " << inf << "\n";
    std::cout << "-1.0/0.0     : " << -inf << "\n";
    std::cout << "inf - inf    : " << inf - inf << "\n";   // NaN
    std::cout << "0.0/0.0      : " << nol / nol << "\n";   // NaN

    return 0;
}

Output yang Dihasilkan:

text
bit lengkap  : 00111101110011001100110011001101
sign         : 0
exponent     : 01111011 (= 123, bias 127 -> -4)
mantissa     : 10011001100110011001101
0.1f         : 0.100000001
0.1 (double) : 0.10000000000000001
1.0/0.0      : inf
-1.0/0.0     : -inf
inf - inf    : -nan
0.0/0.0      : -nan

Bedah baris-per-baris:

  1. Baris 10-12 (float nilai = 0.1f; ... std::memcpy(&bit, &nilai, sizeof bit);): std::memcpy dari <cstring> menyalin 4 byte memori float apa adanya ke std::uint32_t tanpa konversi nilai — trik standar untuk melihat representasi bit mentah (yang lebih “resmi” di C++20 adalah std::bit_cast).
  2. Baris 14 (std::bitset<32>(bit)): std::bitset dari <utility> mengubah integer menjadi rangkaian karakter 0/1, sehingga 32 bit tampil berurutan dari yang paling berbobot hingga paling kecil.
  3. Baris 15-17 (unsigned sign = bit >> 31; ...): tiga baris ini memotong pola bit sesuai tabel anatomi — geser 31 bit untuk sign, geser 23 lalu topeng 0xFF untuk exponent, dan topeng 0x7FFFFF untuk 23 bit mantissa.
  4. Baris 19-21 (std::cout << "exponent : " ...): field exponent terbaca 01111011 = 123. Dikurangi bias 127, eksponen sesungguhnya adalah -4 — persis pangkat pada 0,1 = 1,6 × 2⁻⁴.
  5. Baris 22 (std::bitset<23>(mantis)): mantissa menampilkan pola 10011001100...1101 — jejak jelas pecahan biner berulang dari 0,6 yang dipotong. Perhatikan bit terakhir 1: hasil pembulatan ke atas, yang membuat nilai tersimpan sedikit lebih besar dari 0,1.
  6. Baris 25-28 (std::setprecision(...max_digits10...)): max_digits10 adalah jumlah digit desimal yang diperlukan agar konversi desimal → biner → desimal kembali ke nilai asli (9 untuk float, 17 untuk double). Dengan presisi itu, 0.1f jujur tercetak 0.100000001 dan 0.1 bertipe double tercetak 0.10000000000000001 — keduanya bukan 0,1 yang persis.
  7. Baris 31-36 (double nol = 0.0; ... nol / nol): pembagian floating-point dengan nol tidak menghentikan program seperti pada integer, melainkan menghasilkan nilai khusus: inf untuk pembagian bertanda sama, -inf untuk bertanda berlawanan, dan nan (not a number) untuk operasi tak tentu seperti inf - inf dan 0.0/0.0.

Infinity dan NaN Bekerja Deterministik #

Ketiga baris terakhir output membuktikan bahwa nilai khusus ini mengalir dengan aturan yang pasti, bukan acak: inf tetap inf bila ditambah angka berapa pun, inf - inf jatuh ke nan karena pertanyaannya tidak punya jawaban (“infinity dikurangi infinity”), dan nan menular ke setiap operasi yang disentuhnya. Perilaku baku ini disebut quiet NaN propagation dan berlaku sama di semua platform IEEE 754 — program Anda tidak crash, tetapi hasil akhir bisa berupa nan tanpa peringatan apa pun. Karena itu, perhitungan panjang yang menghasilkan nan atau inf sebaiknya diperiksa di titik-titik penting, bukan sekadar di akhir.

Memahami representasi bit membuat chapter-chapter berikutnya terasa masuk akal: kenapa 0.1 + 0.2 tidak sama persis dengan 0.3, kenapa menambah 1 ke angka 16 digit bisa tidak berpengaruh, dan bagaimana membandingkan floating-point dengan benar. Semua itu adalah babak lanjutan dari satu sumber yang sama — presisi terbatas mantissa — dan kita uraikan satu per satu di Chapter Floating Point Precision.

Rujukan resmi untuk topik Chapter ini: overview numerik C++ , std::bitset , dan fundamental types di cppreference.