Data Representation
Middle SchoolA positional number system in base b gives digit d at position i the weight d · bⁱ. Binary is base 2, so 1011₂ = 1·8 + 0·4 + 1·2 + 1·1 = 11₁₀. Eight bits form a byte, representing 0–255. Because long binary strings are error-prone for humans, we group them in fours as hexadecimal (base 16, digits 0–9 and A–F): one hex digit = exactly four bits.
To represent negative integers, modern hardware uses two's complement: negate a number by inverting all bits and adding 1. In n bits this represents the range −2ⁿ⁻¹ … 2ⁿ⁻¹−1, and — crucially — the same adder circuit from Chapter 1 handles both signs with no modification.
Real numbers use floating point (IEEE 754): a value is stored as sign × mantissa × 2^exponent, like scientific notation in binary. This trades exactness for enormous range. Text is mapped to integers by an encoding — ASCII (7 bits) or Unicode/UTF-8 (variable width) — and colour is typically three bytes: red, green, blue.
Two's complement feels like a trick until you see why it works: it is arithmetic modulo 2ⁿ. On a wheel of 2ⁿ positions, subtracting is the same as adding the complement and letting the top carry fall off the edge. That is why one circuit adds and subtracts, positive and negative alike — the elegance that made it universal.
Floating point explains a famous surprise: 0.1 + 0.2 ≠ 0.3 exactly. One-tenth has no finite binary expansion, just as one-third has no finite decimal expansion, so it is stored slightly rounded. The result is right to about sixteen significant digits — never confuse "computer arithmetic" with "exact mathematics."
Key distinction. Bits carry no inherent meaning. An encoding is a promise about how to read them. Reading UTF-8 bytes as a JPEG, or an integer as a float, is not an error the hardware detects — it is simply a broken promise.
We will convert between bases by hand-coded logic, then compute a two's-complement negation and confirm that 5 + (−5) = 0 in 8 bits.
# --- Base conversion: decimal integer -> binary string ---
def to_binary(n, width=8):
if n == 0:
return "0" * width
bits = ""
while n > 0:
bits = str(n % 2) + bits # prepend remainder (LSB found first)
n = n // 2
return bits.rjust(width, "0")
def from_binary(bits):
value = 0
for ch in bits: # process most-significant bit first
value = value * 2 + int(ch) # Horner's method
return value
# --- Two's complement negation in a fixed width ---
def negate(bits):
inverted = "".join("1" if c == "0" else "0" for c in bits) # invert
return to_binary(from_binary(inverted) + 1, len(bits)) # add 1
def add8(a_bits, b_bits):
total = (from_binary(a_bits) + from_binary(b_bits)) % 256 # mod 2^8
return to_binary(total, 8)
five = to_binary(5) # 00000101
neg_five = negate(five) # 11111011 (== 251 unsigned, == -5 signed)
print(five, neg_five)
print(add8(five, neg_five)) # -> 00000000 : the carry falls off the top
# Text: a character is just its code point
print(ord('A'), to_binary(ord('A'))) # 65 01000001
Notice the loop in from_binary is Horner's method — multiply-by-base then add — the same technique used to evaluate polynomials efficiently. The mod-256 in add8 is the wheel: the overflow carry simply vanishes, giving zero.
Q1 Why does one hexadecimal digit equal exactly four bits?
Because 16 = 2⁴. Each hex digit ranges over 16 values, which is precisely the number of patterns four bits can make. So 0xF3 is 1111 0011 — you can convert nibble by nibble with no arithmetic, which is why hex is the programmer's shorthand for raw memory.
Q2 In 8-bit two's complement, what does 10000000 represent?
−128, the most negative value. Its magnitude has no positive counterpart in the range (which stops at +127), so 10000000 is its own negation on paper — a classic overflow edge case that careful code must guard against.
Q3 Why can't floating point store 0.1 exactly?
Because 0.1 = 1/10, and 10 has the prime factor 5, which is not a power of 2. In binary its expansion repeats forever (0.0001100110011…), so it must be rounded to fit the finite mantissa. Values that are sums of powers of two — like 0.5 or 0.75 — are stored exactly.
Q4 What is the difference between ASCII and UTF-8?
ASCII assigns 128 characters to single 7-bit codes — enough for English but nothing else. UTF-8 is a variable-length encoding of Unicode's ~150,000 characters: it uses one byte for the ASCII range (so it is backward compatible) and two to four bytes for everything else, from "é" to "字" to "😀".
How the ideas connect
Every key idea in this chapter, branching from the core concept — use it to see the whole picture at a glance.
The key facts, visualised
Worked problems, step by step
Follow each solution line by line, then try to reproduce it on paper before moving on.
Example 1Convert decimal 13 to 8-bit binary.
- 13 = 8 + 4 + 1.
- Set the 8, 4 and 1 place bits.
Example 2Convert binary 1111 to hexadecimal and decimal.
- 1111 = 8 + 4 + 2 + 1 = 15.
- 15 in hex is F.
Now you try
Work each one out first, then tap to reveal the worked answer.