C fundamentals ยท lesson 02

Types and memory

A type isn't a container. It's a lens. It tells the CPU how to interpret a sequence of bytes it would otherwise have no opinion about.

in progress
10 min interactive

Memory is just bytes

Your computer's RAM is a long array of bytes. Each byte has an address, starting at 0. That's it. The hardware doesn't know what a "string" is. It doesn't know what a "float" is. It only knows bytes.

Types exist in the compiler's world, not the machine's world. When you write int x = 42;, the compiler reserves 4 bytes at some address and stores the binary representation of 42 there. When you read x, the compiler generates instructions that read 4 bytes from that address and interpret them as a signed integer. The bytes don't know what they represent. The type decides.

๐Ÿ’ก
Mental model: memory is a raw array of bytes. Types are instructions to the compiler saying "read N bytes starting here and interpret them this way." The same bytes can mean different things with a different type.

The primitive types

On a 64-bit system (which is what you're almost certainly running), the sizes are fixed. Memorize these. You'll use them every time you think about memory.

c
char c = 'A'; // 1 byte โ€” integers 0โ€“255 (or โˆ’128โ€“127) short s = 1000; // 2 bytes โ€” integers up to ~32,000 int i = 42; // 4 bytes โ€” integers up to ~2 billion long l = 123456; // 8 bytes โ€” integers up to ~9 quintillion float f = 3.14f; // 4 bytes โ€” 7 significant decimal digits double d = 3.14159; // 8 bytes โ€” 15โ€“16 significant decimal digits printf("%zu\n", sizeof(i)); // sizeof tells you the size in bytes
โš ๏ธ
Sizes are platform-dependent. int is 4 bytes on every platform you'll realistically use, but the C standard only guarantees it's at least 16 bits. If you need exact sizes, use <stdint.h>: int32_t, uint64_t, etc.

How integers are stored

An int is 4 bytes. The value 42 in binary is 0b00101010. Spread across 4 bytes in little-endian order (least significant byte first โ€” what x86 uses):

c
int x = 42; // In memory at address 0x1000 (little-endian): // addr 0x1000: 0x2a (42 in hex = 0x0000002a) // addr 0x1001: 0x00 // addr 0x1002: 0x00 // addr 0x1003: 0x00 // You can see the raw bytes with a pointer cast: unsigned char *p = (unsigned char *)&x; for (int i = 0; i < 4; i++) printf("%02x ", p[i]); // prints: 2a 00 00 00

How floats are stored

Floats use an entirely different encoding than integers. A 32-bit float splits its 4 bytes into three fields:

text
IEEE 754 single-precision (32-bit float): 31 30โ€“23 22โ€“0 โ”Œโ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Sโ”‚ exponent โ”‚ mantissa โ”‚ โ”‚ 1โ”‚ 8 bits โ”‚ 23 bits โ”‚ โ””โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ S = sign (0 = positive, 1 = negative) exponent = power of 2, stored with +127 bias mantissa = fractional digits in binary (the "1." is implicit) value = (โˆ’1)^S ร— 2^(exponent โˆ’ 127) ร— 1.mantissa

For 3.14f: the sign is 0, the exponent encodes 2ยน = 2, and the mantissa encodes the fractional part of 1.57 (since 3.14 = 1.57 ร— 2ยน). The raw hex stored in memory is 0x4048F5C3.

The problem: most decimal fractions have no exact binary representation. 0.1 in decimal is 0.0001100110011โ€ฆ repeating in binary โ€” like 1/3 repeating in decimal. The float stores the closest 23-bit approximation. That approximation error is why 0.1f + 0.2f is not exactly 0.3f.

๐Ÿšซ
Never compare floats with ==. 0.1f + 0.2f == 0.3f is false on every platform. Compare with a tolerance: fabsf(a - b) < 1e-6f. The right epsilon depends on the magnitude of your values โ€” a global constant tolerance breaks on very large or very small numbers.
๐Ÿ’ก
Use double unless you have a reason not to. A double has 52 mantissa bits vs 23 for float โ€” roughly 15โ€“16 significant decimal digits vs 7. The cost is 8 bytes instead of 4. On modern CPUs, double arithmetic is not slower than float. Use float for large arrays where memory and bandwidth matter (e.g., GPU work).

There's much more to floating-point: special values (Inf, NaN, -0.0), machine epsilon, catastrophic cancellation, and the rules for when the compiler is allowed to reorder float operations. That all belongs in its own lesson โ€” see Floating-point numbers (lesson 12).

Signed vs unsigned

Every integer type has a signed and unsigned variant. The same 4 bytes mean different things:

c
unsigned int u = 4294967295; // 0xFFFFFFFF โ€” max value int s = -1; // also 0xFFFFFFFF in memory! // Same 4 bytes. Different type. Different interpretation. // This is the type-as-lens model in action. unsigned int wrapped = 0 - 1; // = 4294967295 (wraps around โ€” defined) int overflow = 2147483647 + 1; // undefined behavior!
๐Ÿšซ
Signed integer overflow is undefined behavior in C. Unsigned overflow wraps around (defined). Signed overflow does not โ€” the compiler can assume it never happens and optimize around it. We'll cover this in lesson 10.

The visualization

The interactive below shows how different types lay out in memory โ€” byte by byte. Click any byte to see what it represents and where it sits.

type memory layout โ€” byte by byte
Click a tab above to explore each type's memory layout. Click any byte for details.
one-line takeaway

Types don't change the bytes โ€” they change how you read them.