Computer Organisation & Architecture (COA) · Lesson 2 of 8 · Updated ·
Data Representation — Binary, Hex, Two's Complement, Overflow and IEEE 754 Floating Point
Inside the computer everything is bits. How those bits are interpreted — unsigned, signed, floating-point, character — is decided by the representation. Exams love these conversions, and real bugs (integer overflow, 0.1 + 0.2 != 0.3) come straight from them.
1. Binary and hexadecimal
Each binary digit is a power of 2. 1011₂ = 8 + 0 + 2 + 1 = 11. Hexadecimal groups 4 bits into one digit (0–9, A–F), which is why memory addresses and colours are written in hex.
n = 202
print("binary:", bin(n), "| hex:", hex(n), "| octal:", oct(n))
print("1011 0110 as decimal:", int("10110110", 2))
print("0xCA as decimal:", int("CA", 16))
print("hex digit groups of 11001010:", "1100 ->", hex(0b1100), "| 1010 ->", hex(0b1010))
binary: 0b11001010 | hex: 0xca | octal: 0o312
1011 0110 as decimal: 182
0xCA as decimal: 202
hex digit groups of 11001010: 1100 -> 0xc | 1010 -> 0xa
2. Signed numbers: three ways
How do we store −5 in 8 bits?
| Method | +5 | −5 | Zeros | Range (8 bits) |
|---|---|---|---|---|
| Sign-magnitude | 0000 0101 | 1000 0101 | two (+0, −0) | −127 to +127 |
| 1's complement (flip all bits) | 0000 0101 | 1111 1010 | two | −127 to +127 |
| 2's complement (flip + 1) | 0000 0101 | 1111 1011 | one | −128 to +127 |
Every modern CPU uses 2's complement: one zero, and addition/subtraction use the same adder circuit for signed and unsigned numbers.
A neat way to read a 2's complement number: the most significant bit has negative weight. 1111 1011 = −128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = −5.
def to_twos(value, bits=8):
return format(value & (2 ** bits - 1), f"0{bits}b")
def from_twos(bitstr):
v = int(bitstr, 2)
return v - 2 ** len(bitstr) if bitstr[0] == "1" else v
for v in [5, -5, -1, 127, -128]:
b = to_twos(v)
print(f"{v:5} -> {b} -> back to {from_twos(b)}")
5 -> 00000101 -> back to 5
-5 -> 11111011 -> back to -5
-1 -> 11111111 -> back to -1
127 -> 01111111 -> back to 127
-128 -> 10000000 -> back to -128
Range of n-bit 2's complement: −2ⁿ⁻¹ to 2ⁿ⁻¹ − 1. For 8 bits: −128 to 127; 16 bits: −32,768 to 32,767; 32 bits: about ±2.1 billion.
3. Overflow
If the true result doesn't fit in the range, the stored result wraps around — overflow. Rule for 2's complement addition: overflow happens only when two numbers of the same sign give a result of the opposite sign (equivalently, carry into the sign bit ≠ carry out of it).
def add8(a, b):
raw = (a + b) & 0xFF
result = raw - 256 if raw >= 128 else raw
overflow = (a >= 0) == (b >= 0) and (result >= 0) != (a >= 0)
return result, overflow
for a, b in [(100, 20), (100, 50), (-100, -50), (100, -50)]:
r, ov = add8(a, b)
print(f"{a:5} + {b:4} = {r:5} overflow={ov}")
100 + 20 = 120 overflow=False
100 + 50 = -106 overflow=True
-100 + -50 = 106 overflow=True
100 + -50 = 50 overflow=False
100 + 50 "became" −106 in 8 bits. Real bugs like this have crashed rockets and frozen games; languages like Java silently wrap int the same way at 32 bits.
4. IEEE 754 floating point
Real numbers use scientific notation in binary: (−1)^sign × 1.mantissa × 2^(exponent − bias).
Single precision (32 bits): 1 sign bit, 8 exponent bits (bias 127), 23 fraction (mantissa) bits. Double precision: 1 + 11 (bias 1023) + 52.
Worked example: −6.75
- Binary: 6.75 =
110.11₂ - Normalise:
1.1011 × 2² - Sign = 1 (negative)
- Exponent = 2 + 127 = 129 =
1000 0001 - Fraction = bits after the point:
1011followed by zeros
Let Python confirm it bit by bit:
import struct
def ieee754(x):
bits = format(struct.unpack(">I", struct.pack(">f", x))[0], "032b")
sign, exp, frac = bits[0], bits[1:9], bits[9:]
print(f"{x:>8}: {sign} {exp} {frac} (hex {hex(int(bits, 2))})")
e = int(exp, 2)
value = (-1) ** int(sign) * (1 + int(frac, 2) / 2 ** 23) * 2 ** (e - 127)
print(f" exponent {e} - 127 = {e - 127}, decoded back = {value}")
ieee754(-6.75)
ieee754(0.1)
-6.75: 1 10000001 10110000000000000000000 (hex 0xc0d80000)
exponent 129 - 127 = 2, decoded back = -6.75
0.1: 0 01111011 10011001100110011001101 (hex 0x3dcccccd)
exponent 123 - 127 = -4, decoded back = 0.10000000149011612
0.1 has no exact binary representation (like 1/3 in decimal), so the stored value is very slightly off — the root of 0.1 + 0.2 != 0.3 in every language.
Special values:
| Exponent bits | Fraction | Meaning |
|---|---|---|
| all 0 | 0 | ±0 |
| all 0 | ≠ 0 | denormal (tiny numbers) |
| all 1 | 0 | ±∞ |
| all 1 | ≠ 0 | NaN (not a number) |
5. Characters
- ASCII: 7 bits, 128 characters (
'A'= 65). - Unicode / UTF-8: every character in every script; UTF-8 uses 1 byte for ASCII and 2–4 bytes for others — Hindi letters take 3 bytes each.
for ch in ["A", "₹", "क"]:
print(ch, "code point", ord(ch), "UTF-8 bytes:", len(ch.encode("utf-8")))
A code point 65 UTF-8 bytes: 1
₹ code point 8377 UTF-8 bytes: 3
क code point 2325 UTF-8 bytes: 3
Common mistakes
- Forgetting to add 1 when taking 2's complement.
- Using range −127..127 for 8-bit 2's complement (it's −128..127).
- Forgetting the bias (127) or the implied leading 1 in IEEE 754.
- Declaring overflow when adding numbers of opposite signs (impossible in 2's complement).
Quick recap
- Hex = groups of 4 bits.
- 2's complement = invert + 1; MSB has weight −2ⁿ⁻¹; range −2ⁿ⁻¹ … 2ⁿ⁻¹−1; one zero.
- Overflow: same-sign operands, opposite-sign result.
- IEEE 754 single: 1 | 8 (bias 127) | 23, value = (−1)ˢ × 1.f × 2^(e−127).
- 0.1 isn't exact in binary; UTF-8 uses 1–4 bytes per character.