Information Representation Explained
September 2, 2026 • 11 min read

Part of the series:Computers Explained
At the lowest level, a computer knows only two things: on and off. Every photo, song, spreadsheet, and video call is, underneath it all, a long sequence of ones and zeros. But a pile of bits means nothing by itself. What gives bits meaning is representation: the agreed-upon conventions that map bit patterns to numbers, text, and richer data structures.
This post walks through those conventions from the ground up: binary and hexadecimal1, unsigned and signed integers, the elegant trick of two’s complement2, floating-point numbers3, characters and Unicode4, and finally how all of it composes into the data structures programs actually use.
Bits, binary, and hexadecimal
A bit is a single binary digit: 0 or 1. Eight bits form a byte, which is the smallest addressable unit of memory on most modern machines. With n bits you can represent 2^n distinct values: 1 bit gives 2 values, 8 bits give 256, 32 bits give about 4.3 billion.
Humans rarely read raw bits, so we group them. Binary1 is base 2, where each position is a power of two:
1 1 0 1 0 1 0 0
128 + 64 + 0 + 16 + 0 + 4 + 0 + 0 = 212
Hexadecimal5 (base 16) exists for one practical reason: a single hex digit maps to exactly four bits (a nibble), so it compresses binary without the mental gymnastics of decimal conversion:
binary: 1101 0100
hex: D 4 -> 0xD4 (212 in decimal)
That is why you see hex everywhere in computing: memory addresses (0x7ffee4a3b8c0), color codes (#FF5733), and byte dumps. The 0x prefix (and 0b for binary, as in 0b11010100) is just a notational convention popularized by C and adopted by many later languages. Whenever you see hex, read it as shorthand for bits, not as a different kind of number.
New to number base conversions? See the dedicated step-by-step guide: Binary, Decimal, and Hex Conversions 101.
How numbers are stored
Numbers are stored as fixed-width bit patterns. The width (uint8, uint16, uint32…) decides the range. Three cases: never negative, possibly negative, and fractional.
Unsigned integers
Same binary as above. Use it for values that can never be negative: counts, sizes, pixel brightness. Range is 0 to 2^n - 1 (uint8: 0-255, uint32: 0-4,294,967,295).
It wraps like an odometer: uint8 255 + 1 = 0. Hardware does arithmetic modulo 2^n; languages differ in how they expose overflow.
Signed integers
For values that can be negative: temperature, balance, offsets. Use two’s complement2: to negate, invert every bit and add one.
+4 = 0000 0100
flip: 1111 1011
Add one, bit by bit from the right. A bit only holds 0 or 1, so 1 + 1 = 2 does not fit, write 0 and carry 1 to the left:
1111 1011
+ 0000 0001
----------
1111 1100 = -4
Right to left:
-> 1 + 1 = 2 -> 0, carry 1
-> 1 + 0 + carry 1 = 2 -> 0, carry 1
-> 0 + 0 + carry 1 = 1
-> 1 + 0 = 1
-> 1 + 0 = 1
-> 1 + 0 = 1
-> 1 + 0 = 1
-> 1 + 0 = 1
Adding them gives 0000 0100 + 1111 1100 = 1 0000 0000, the 9th bit vanishes, leaving 0. Same adder works for signed and unsigned. Side effects: 8-bit range is -128 to +127 (asymmetric), single zero, top bit reads as sign (1 = negative).
Floating point: IEEE 754
Computers can’t store a floating number in a single block of memory, so they split it into 3 parts:
- sign (1 bit)
- exponent (8 bits)
- fraction (23 bits)
So how do we transform a number like 5.75 into binary? We must write it in scientific notation in base 2:
1. Decimal to binary
Split integer and fractional parts:
5.75
- 5 -> stay 5 = 101 (binary)
- 0.75 -> 0.75 * 2 = 1.5 -> stay 1 = 1 (binary), carry 0.5
- 0.5 -> 0.5 * 2 = 1 -> stay 1 = 1 (binary)
=> 101.11 (binary)
2. Normalize to 1.xxx * 2^exp
- Simply move the point until there is only a single
1on the left. - The exponent is how many positions we had to move it. Here 2 positions to the left:
101.11 = 1.0111 * 2^2, so exponent = 2
3. Fill the 3 fields
1. Sign (1 bit) -> The Sign Bit
Look at the whole number itself (+5.75). Is it positive or negative?
- Positive ->
0(if it were-5.75, this bit would be1).
2. Exponent (8 bits) -> The Exponent Field
You already mastered this! Shift count = 2. Add bias: 2 + 127 = 129. In binary, grouped in nibbles:
129 = 1000 0001
3. Fraction (23 bits) -> The Mantissa Field
Look at the normalized binary string: 1.0111. Break that string at the dot:
- The
1.before the dot: this is the implicit part. It is NOT written into memory. - The
0111after the dot: this is the actual fraction. You take these exact 4 bits and copy them directly into the fraction field:0111. Then fill the remaining empty slots with zeros to reach 23 bits, grouped in nibbles:
0111 0000 0000 0000 0000 000
Putting it together side-by-side:
Normalized: (+) 1. 0111 * 2^2
│ │ │ │
▼ ▼ ▼ ▼
Field: Sign[ ] Exponent (+127) Fraction
Stored: 0 1000 0001 01110000000000000000000
▲
(Implicit 1. is dropped!)
Final 32 bits, grouped in nibbles:
bits: 0100 0000 1011 1000 0000 0000 0000 0000 = 0x40B80000
Special patterns & practical lessons
Special values (IEEE 754 reserves):3
- Exponent all zeros (
0000 0000): signed zero (+0.0/-0.0) or subnormals, extremely small values near zero where the implicit leading bit becomes0.instead of1.. - Exponent all ones (
1111 1111):- Fraction all zeros -> Infinity (
+infor-inf). - Fraction non-zero -> NaN (Not a Number, e.g.
0.0 / 0.0orsqrt(-1)). NaN compares unequal to everything, including itself.
- Fraction all zeros -> Infinity (
Two golden rules for developers:6
- Never compare floats with
==. Most decimal fractions (like0.1) repeat infinitely in binary.0.1 + 0.2becomes0.30000000000000004, so exact equality checks fail due to small rounding differences. - Floats are for measurements, not money. Rounding error accumulates. Financial systems use integer cents or dedicated decimal types to avoid losing money to floating-point drift. A 64-bit
double(1 sign + 11 exponent + 52 fraction bits) helps, but does not eliminate rounding error.
How other data is stored
Beyond numbers, everything is patterns agreed by convention, like ASCII mapping numbers to letters.
Characters and Unicode
Text is stored as numbers for letters: the industry agrees which number means which letter.
ASCII7 (1963) assigned the English alphabet, digits, and control codes to 7-bit numbers 0–127: A is 65, a is 97. One byte per character, beautifully simple yet useless for the rest of the world’s writing systems. The following decades produced a chaos of incompatible 8-bit extensions (“code pages”) where the same byte meant é in one country and Ω in another.
Unicode4 fixed this by separating two concerns: a universal catalog assigning a permanent number to each entry (a code point, written U+00E9 for é), and encodings that map code points to bytes. The dominant encoding is UTF-88, and its design is a small masterpiece:
U+0000–U+007F: 0xxxxxxx (ASCII, 1 byte)
U+0080–U+07FF: 110xxxxx 10xxxxxx (2 bytes)
U+0800–U+FFFF: 1110xxxx 10xxxxxx 10xxxxxx (3 bytes)
U+10000–U+10FFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (4 bytes)
The x bits carry the code point; the leading bits announce the length. Example: é is U+00E9 = 1110 1001 in binary, which fits the 2-byte template as 110_00011 10_101001 = bytes 0xC3 0xA9. The euro sign € (U+20AC) needs three bytes: 0xE2 0x82 0xAC.
Three properties made UTF-8 win: ASCII text is valid UTF-8 unchanged (backward compatible), no ASCII byte can appear as part of a multi-byte sequence, making ASCII delimiters and control bytes easy to recognize, and you can resynchronize mid-stream after corruption.
(UTF-169 code units are used by Java and JavaScript, and Windows APIs use UTF-16 extensively; it encodes text in 2-byte units with surrogate pairs for characters beyond U+FFFF, which is why "😀".length is 2 in JavaScript.)
Why did systems like Java, JavaScript, and Windows adopt UTF-16?
In the early 1990s, Unicode was originally designed as a fixed 16-bit encoding (UCS-2) supporting 65,536 characters (U+0000 to U+FFFF). Engineers believed 16 bits would be enough for all human writing systems forever, offering fast, constant-time indexing.
When Unicode later expanded beyond 65,536 characters, UCS-2 evolved into UTF-16 using surrogate pairs, but by then, these major platforms were already locked into 16-bit core runtimes.
Classic text bugs all trace back to this layer: mojibake (decoding bytes with the wrong encoding), confusing character count with byte count, and assuming one visible glyph equals one code point (accents and emoji can combine several).
Representing data: from bytes to meaning
Lists, records, images and sound are compositions of the above, plus endianness10: the agreed order in which the bytes of a multi-byte value like 0x12345678 are stored in memory. Getting it wrong silently garbles every number, which is why formats like PNG and network byte order conventions specify byte order explicitly.
- Little-endian: least-significant-byte-first. The value
0x12345678is stored as78 56 34 12. This is what your personal computer uses: x86 (Intel/AMD) and ARM chips in little-endian mode store multi-byte integers this way in RAM. It is the “host byte order” for most desktops, laptops, and phones. - Big-endian: most-significant-byte-first. The same value is stored as
12 34 56 78, in the order humans naturally read hex. This is the standard for network protocols and many file formats: TCP/IP declares big-endian as “network byte order”, so every port number, length field, and address traveling over the wire uses it.
What happens when you send a message right now? When you type on your little-endian laptop and click send, your bytes must be translated twice:
[ Your Device ] [ Network Wire ] [ Server / Target ]
Little-Endian Big-Endian Little-Endian
(x86 / ARM) (Network Byte Order) (x86 / ARM)
RAM/CPU Network Interface Card RAM/CPU
0x78 0x56 0x34 0x12 ---htons()--> 0x12 0x34 0x56 0x78 ---ntohs()--> 0x78 0x56 0x34 0x12
- Local processing (your device): your CPU builds packet headers (length, port numbers) in little-endian in RAM.
- Translation (
htons()/htonl()): before handing data to the network card, software callshtons()(host-to-network-short) orhtonl()(host-to-network-long). These flip multi-byte integers from little-endian to big-endian. - Transmission over the wire: the data travels across the internet in big-endian network byte order.
- Reception and decoding: the receiving server calls
ntohs()(network-to-host-short) to flip the bytes back into little-endian so its own x86 or ARM CPU can read the numbers correctly.
Common building blocks:
- Booleans and enums: often a single byte holding
0/1(sometimes packed into bits or using larger units depending on the language and ABI); enums map names to small integers. - Arrays: elements laid out back-to-back; indexing is just
base_address + index * element_size. - Structs/records: fields concatenated, often with padding bytes inserted so each field lands on an aligned address the CPU can read efficiently. A struct’s size is not always the sum of its fields.
- Strings: either length-prefixed (Pascal style: length first, then bytes) or null-terminated (C style: bytes ending in
0x00), the latter being a classic source of buffer overflows. - Media: a pixel is commonly three bytes (red, green, blue) in basic RGB; audio is thousands of amplitude samples per second; video is images plus math that exploits what eyes won’t notice. JPEGs and MP3s are just very clever agreements about which details to throw away.
And the deepest lesson of all: a type is an interpretation, not a property of the bits. The 32 bits 0x40490FDB are the float 3.1415927 and simultaneously the unsigned integer 1078530011. The bits never changed; only the lens did. Every cast, every reinterpret, every debugger “view as” command is a reminder that meaning lives in the convention, agreed upon by whoever wrote the bytes and whoever reads them.
Footnotes
/sponsor
Enjoyed this post? You can sponsor me and this site.