The edge of a fixed-width number
In the last two guides you learned to read a binary and hexadecimal bit pattern and to make negative numbers fall out of two's complement. Both rested on a quiet assumption we now have to face head-on: the number has only so many bits. An `unsigned char` is 8 bits, a `uint32_t` is exactly 32. That fixed width is the whole point of a fixed-width type — but it means every integer in C lives inside a finite box, and a box has walls.
Take the smallest box, an 8-bit unsigned value. It can hold 0 up through 255 — that is 256 distinct patterns, from 0x00 to 0xff, and not one more. So what does the hardware do when you add 1 to 255? There is no bit number 8 to carry into; the box is full. The answer is the central fact of this whole guide: it does not stop, it does not warn you, it simply wraps around to 0. Counting past the top of the range lands you back at the bottom, like a car odometer rolling over from 999999 to 000000.
The cleanest picture is a ring, not a number line. Lay the 256 patterns 0x00 through 0xff around a clock face: 254, 255, and then — instead of a mythical 256 — you are back at 0, with 1 and 2 following. Going up off the top (255 + 1) lands on 0; going down off the bottom (0 - 1) lands on 255. There is no edge to fall off, only a seam you cross without feeling it. Hold that ring in mind: everything in this guide is a consequence of integers being circular rather than open-ended.
Unsigned wrap: defined, predictable, modular
For unsigned integers, that ring is not a bug — it is exactly what the C standard promises. Unsigned arithmetic is defined to be modular: every result is reduced "modulo 2^N", where N is the width in bits. For our 8-bit value, every answer is taken mod 256. So 255 + 1 is 0, and 0 - 1 is 255, and 200 + 100 is 300 - 256 = 44. Nothing is undefined here; the hardware just keeps the low N bits and throws the carry away. This is unsigned wraparound, and you are allowed to rely on it.
That "0 - 1 == 255" is worth pausing on, because it bites real programs. Suppose you write a loop with `size_t i` (an unsigned type) and try to count down with `for (i = n - 1; i >= 0; i--)`. Since `i` is unsigned, the test `i >= 0` is always true — it can never go negative. When `i` is 0 and you do `i--`, it wraps to a gigantic value like 18446744073709551615, the loop keeps going, and you charge off the end of your array. The fix is to count up, or to use a signed counter on purpose. The wrap is well-defined; the bug is expecting it to behave like math on paper.
Signed overflow: the same wall, a much worse fall
Now do the same thing with a signed type and the story changes completely. A `signed char` in two's complement runs from -128 to +127. The bit pattern 0x7f is +127, the largest positive value; its sign bit (the top bit) is still 0. Add 1 and the pattern becomes 0x80 — but in two's complement 0x80 reads as -128. So at the machine level, `127 + 1` flips a positive number into the most negative one. That sudden sign flip is signed integer overflow, and it is a far nastier creature than unsigned wrap.
Why nastier? Because in C, signed integer overflow is undefined behavior — and that is not a synonym for "wraps to -128 like the hardware does." This is the single most important honesty in this guide. Undefined behavior means the standard places no requirement on what happens, and the optimizer is allowed to assume it simply never occurs. So a compiler can legally take a check like `if (x + 1 < x)` — your attempt to detect the overflow — and delete it entirely, reasoning "signed overflow can't happen, so `x + 1` is always greater than `x`, so this branch is dead." Your guard vanishes, and the bug it was guarding against walks right through.
A trap hidden in promotion
One more place overflow ambushes beginners: it can happen even when both operands and the result look small enough. Before C does most arithmetic, it widens small types to `int` — this is integer promotion. Multiply two `unsigned char` values like `200 * 200` and they are first promoted to `int`, so the multiply is `int` arithmetic giving 40000, not a wrapped 8-bit answer. Helpful here — but the very same promotion rules can turn an unsigned comparison signed, or do the math in a width you did not expect, so the type of the surrounding expression, not just the variables, decides whether you wrap and whether that wrap is defined.
The practical defence is to know your widths and reach for them by name. The fixed-width types in `<stdint.h>` — `uint8_t`, `int32_t`, `uint64_t` — say precisely how big a value is on every platform, so you are never guessing whether `long` is 4 or 8 bytes. When a value might exceed a width, do the arithmetic in a wider type first (`(uint64_t)a * b`) and only then store it back. Most overflow bugs are really "I did the math in the wrong-sized box" bugs.
Endianness: which byte comes first
Inside one register the bits have a clear order — most-significant on the left, least-significant on the right. But the moment a multi-byte value is written out to memory, a new question appears that a single register never raises: in what order do its bytes sit at consecutive addresses? That ordering is endianness, and it is purely a convention about layout, not about the value. The number 0x0a0b0c0d is the same number on every machine; the disagreement is only about which of its four bytes lands at the lowest address.
store the 32-bit value 0x0a0b0c0d at address 0x1000:
little-endian (x86-64, ARM default): low byte first
addr 0x1000 0x1001 0x1002 0x1003
byte 0x0d 0x0c 0x0b 0x0a
big-endian (network order, older RISC): high byte first
addr 0x1000 0x1001 0x1002 0x1003
byte 0x0a 0x0b 0x0c 0x0dLittle-endian (the low-order byte at the lowest address) is what x86-64 and most ARM machines use, so it is what you will see day to day. Big-endian (high byte first) reads more naturally to a human and survives in some older chips. You almost never notice the difference — until two machines that disagree try to exchange raw bytes. Write a `uint32_t` straight to a file or a socket on a little-endian box and read it on a big-endian one, and 0x0a0b0c0d comes back as 0x0d0c0b0a: a completely different number, silently.
This is why protocols pick one fixed convention. The internet standardised on big-endian as network byte order, and the libc helpers `htonl()` / `ntohl()` (host-to-network and back) convert your integers at the boundary so both ends agree. Within one program, in one machine's memory, you can ignore endianness entirely — the CPU reads its own bytes back consistently. It only matters at the edges, where bytes cross from one machine's memory to a file, a socket, or another CPU.