Skip to content
yutils

How Numbers Are Stored (Binary, Hex, Two's Complement, Floats)

Why developers see hex everywhere, how two's complement makes addition and subtraction the same circuit, why 0.1 + 0.2 ≠ 0.3, and the bit patterns hiding inside every integer and float.

~8 min read

#FF0000 (red), 0xDEADBEEF (memory debugging), 0x7FFFFFFF (max 32-bit int) — hex is everywhere for developers. Why do computers love hex? And why does 0.1 + 0.2 ≠ 0.3? This guide walks through binary / hex / two's complement / IEEE 754 — the bit patterns hiding inside every integer and float.

Why bases matter

Computers think in 0/1. Humans don't read 32 bits well:

0011 1001 0101 1010 1110 0100 1100 1100   (32 bits)
   ↑ unreadable at a glance

Same value in different bases:

Binary:  00111001 01011010 11100100 11001100
Hex:     39 5A E4 CC
Decimal: 962,360,012

Hex aligns perfectly with binary (4 bits = 1 hex digit). Best visual compression for binary, which is why developers and machines both like it.

Why hex — the magic of 16 = 2^4

4 bits = exactly one hex digit. Conversion is trivial:

Binary  Hex
0000    0
0001    1
0010    2
0011    3
0100    4
0101    5
0110    6
0111    7
1000    8
1001    9
1010    A
1011    B
1100    C
1101    D
1110    E
1111    F

A 16-bit integer = 4 hex digits. 32-bit = 8 hex digits. 64-bit = 16 hex digits. Direct mapping.

Decimal doesn't have this property — 2^10 = 1024 ≈ 1000, which is why 1 KiB and 1 KB are different. Octal also aligns (3 bits = 1 octal digit), but hex's 4-bit boundary fits the byte better.

Try it — Number Base Converter shows the same value in binary / octal / decimal / hex side by side. Hex Encode / Decode converts text ↔ hex.

Where hex shows up

  • Color codes#RRGGBB, each channel 8 bits (00-FF). #FF6B35 = R 255, G 107, B 53.
  • Memory addresses — debuggers / crash dumps show 0x7FFFEE00
  • SHA / MD5 / UUID — sequences of hex chars
  • Error codes / flags — Windows 0x80004005 (E_FAIL)
  • Bit masks0xFF00 = high byte,0x000F = low nibble
  • UTF-8 bytes — Korean "안" = EC 95 88
  • Magic numbers0xDEADBEEF, 0xCAFEBABE (Java class file), 0x89 50 4E 47 (PNG header)
  • Password hash markers — part of bcrypt's $2b$12$...

Positive + negative = two's complement

Positive integers are easy. 5 = 0101. What about negatives?

Naive approach — sign bit (top bit, 0 = +, 1 = -):

+5 = 0101
-5 = 1101  ← just flip the sign bit?

Problems:

  • Two zeros — +0 (0000) and -0 (1000)
  • Addition becomes complex. +5 + (-5) = -10 in binary, not 0.

Solution — two's complement:

-X = (NOT X) + 1

Example: -5 (4 bit)
  +5  = 0101
  NOT = 1010
  + 1 = 1011

So -5 = 1011

Sanity check — 5 + (-5):
  0101
+ 1011
  ────
  10000  ← 5 bits (overflow); low 4 bits = 0000 = 0 ✓

Key wins:

  • Only one zero (0000)
  • Addition and subtraction share circuitry — no separate subtractor needed, just negate then add.
  • Comparison — check sign bit, then standard compare

Bit width ranges

8-bit (byte):   -128       to     +127         (= -2^7 to 2^7 - 1)
16-bit (short): -32,768    to     +32,767      (= -2^15 to 2^15 - 1)
32-bit (int):   -2.1B       to     +2.1B        (= -2^31 to 2^31 - 1)
64-bit (long):  -9.2 × 10^18 to    +9.2 × 10^18 (= -2^63 to 2^63 - 1)

Positive range is 1 smaller than negative — zero lives on the positive side.

Famous overflow incidents

// 32-bit int max + 1
0x7FFFFFFF + 1 = 0x80000000
            =  2147483647 + 1 = -2147483648 (wraps to negative!)

// Y2038 — Unix timestamp at int32 limit
2038-01-19 03:14:07 UTC is the last second
The next second → -2147483648 → December 13, 1901

// Java's Integer.MIN_VALUE
Math.abs(-2147483648) === -2147483648  ← stays negative!

Floats — the secret of IEEE 754

Integers aren't enough. How do you store 0.1, π, 1.5 × 10^200?

IEEE 754 (1985) — scientific notation in bits:

x = (-1)^sign × 1.mantissa × 2^exponent

32-bit float (single):
  ┌─┬───────────┬─────────────────────────────┐
  │S│ exponent  │           mantissa          │
  │ │  (8 bit)  │           (23 bit)          │
  └─┴───────────┴─────────────────────────────┘

64-bit double:
  ┌─┬──────────────┬──────────────────────────────────────────────────┐
  │S│  exponent    │                  mantissa                        │
  │ │  (11 bit)    │                  (52 bit)                        │
  └─┴──────────────┴──────────────────────────────────────────────────┘

Example — 1.5 as 64-bit double:

1.5 = 1.1₂ × 2^0
S = 0 (positive)
exponent = 0 + 1023 (bias) = 1023 = 01111111111
mantissa = 1000...0 (the binary 0.1 = 0.5)

Full: 0 01111111111 1000000000000000000000000000000000000000000000000000
    = 0x3FF8000000000000

Why 0.1 + 0.2 ≠ 0.3

> 0.1 + 0.2
0.30000000000000004

> 0.1 + 0.2 === 0.3
false

0.1 has no finite binary representation. 1/10 in binary is a repeating fraction:

0.1 (decimal) = 0.0001100110011001100... (binary, repeating)

Truncated to 54 mantissa bits (double):
0.1 ≈ 0.1000000000000000055511151231257827021181583404541015625
0.2 ≈ 0.2000000000000000111022302462515654042363166809082031250

Sum: 0.3000000000000000166533453693773481063544750213623046875
   ≈ 0.30000000000000004

Solutions — BigDecimal / Decimal128 for money. Epsilon for compare:

Math.abs(a - b) < 1e-9   // float comparison

// Or work in integer units
// $0.10 + $0.20 → 10 cents + 20 cents

Special values

NaN (Not a Number):
  0 / 0, Math.sqrt(-1)
  NaN === NaN  → false  (the only value not equal to itself)
  Use isNaN(x) or Number.isNaN(x)

Infinity:
  1 / 0 = Infinity
  -1 / 0 = -Infinity

±0 (positive zero, negative zero):
  0 === -0  → true
  1 / 0 === 1 / -0  → false (Infinity vs -Infinity)
  Object.is(0, -0) → false

Hex prefix conventions

  • 0x — C / JavaScript / Python (0xFF = 255)
  • 0o — Python octal
  • 0b — binary (0b1011)
  • # — CSS colors (#FF6B35)
  • $ — assembly ($FF)
  • &h — VBA / Visual Basic

Underscores for readability: 0xFFFF_FFFF_DEAD_BEEF (Python / Rust / modern JS).

Common pitfalls

1. JavaScript bitwise operators are 32-bit

0x100000000 | 0   // 4294967296 | 0 = 0  ← truncated to 32 bit!

// 64-bit bitwise requires BigInt
0x100000000n | 0n  // 4294967296n

2. parseInt's base guess (older environments)

parseInt("010")   // old browsers: 8 (octal interpretation!)
parseInt("010", 10) // 10  ← always specify radix
parseInt("0xff")   // 255 (hex auto-detected)

3. Signed vs unsigned shift

-1 >> 1   // -1  (sign bit preserved, arithmetic shift)
-1 >>> 1  // 2147483647 = 0x7FFFFFFF (zero fill, logical shift)

4. Float comparison

// Bad
if (price * 1.1 === expected) { ... }

// Good
if (Math.abs(price * 1.1 - expected) < 0.001) { ... }

// Best (financial)
priceInCents * 110 / 100   // integer arithmetic

5. NaN comparison gotcha

NaN === NaN     // false
NaN !== NaN     // true (only way to identify NaN)
isNaN("foo")    // true (strings get coerced!)
Number.isNaN("foo") // false (strict)

References

Summary

  • Binary lives inside the computer. Hex is the compact form we read (4 bits = 1 hex digit).
  • Hex appears in colors, memory, hashes, flags, UTF-8 bytes, magic numbers — everywhere.
  • Two's complement encodes negatives so add/subtract share a circuit, no -0, range -2^(n-1) to 2^(n-1) - 1.
  • Overflow wraps — Y2038 and int32 max + 1 = negative are the famous cases.
  • IEEE 754 floats = sign + exponent + mantissa. 0.1 + 0.2 ≠ 0.3 because 0.1 is a repeating binary fraction.
  • Money belongs in BigDecimal or integer units (cents). Float comparison needs an epsilon.
  • NaN, Infinity, ±0 — IEEE 754 specials, watch the comparison quirks.
  • Try it — Number Base Converter / Hex Encode / Decode.
Back to guides