The same bits, a different reading
In the previous guide you learned to read a string of bits as a plain counting number: each position is a power of two, and you add up the ones. An 8-bit byte read this way runs from 0 up to 255, and that is exactly the unsigned interpretation. But here is the catch a beginner rarely notices: the bits themselves do not contain the answer to 'is this signed or not?'. The byte 0xFF is just eight 1s. Whether those eight 1s mean 255 or mean -1 is decided entirely by the type you declared, not by anything stored in memory.
This is the heart of bit pattern versus interpretation, and it is worth saying slowly because so much later confusion flows from missing it. A box in memory holds a bit pattern — a raw arrangement of 0s and 1s. The interpretation — unsigned integer, signed integer, a character, part of a float — is a lens your program chooses to look through. The CPU stores patterns; your types decide what they say. So when you write `unsigned char` versus `signed char` in C, you are not changing any bits; you are changing the lens.
So our real question is sharp: we have fixed-width boxes of bits — 8, 16, 32, or 64 of them — and we want one of those interpretations to be able to say 'negative'. We cannot reach for a separate minus sign on paper; there is no extra slot. We have to spend some of the very bit patterns we already have to mean negative values. Every scheme for doing this is a trade, and the trick is finding the trade that makes the hardware's job easiest.
Two false starts that nearly work
The most obvious idea is sign-magnitude: steal the leftmost bit to be a sign flag — 0 for plus, 1 for minus — and let the remaining bits hold the size. In a byte, 0000 0101 is +5 and 1000 0101 is -5. Tidy and human-friendly! But it has two ugly problems. First, there are two zeros: 0000 0000 is +0 and 1000 0000 is -0, which wastes a pattern and makes equality checks fiddly. Second, and worse, addition stops being simple: to compute 5 + (-3) the hardware must compare magnitudes, decide which is bigger, subtract, and pick a sign. That is a lot of circuitry for one little sum.
A cleverer near-miss is ones' complement: to negate a number, just flip every bit. So +5 is 0000 0101 and -5 is its flip, 1111 1010. Addition is now much closer to ordinary binary addition, which is promising. But the two-zeros problem stubbornly remains (0000 0000 is +0 and 1111 1111 is -0), and adding still needs an awkward fix-up: a carry out of the top has to be added back in at the bottom, the so-called end-around carry. Both of these schemes are real history, collected under sign-magnitude and ones' complement, and both were genuinely used in early machines. They were abandoned for one reason: a better idea existed.
Two's complement: the winning trick
The scheme every modern CPU uses is two's complement, and its core idea is delightfully sneaky: keep ordinary binary addition exactly as it is, with no fix-ups, and choose the negative patterns so that this just works out. The defining recipe to negate a number is: flip every bit, then add 1. So to get -5 in a byte, start from +5 = 0000 0101, flip to 1111 1010, add 1 to get 1111 1011. That pattern is now what we agree to call -5. Notice -0 vanishes: flip 0000 0000 to 1111 1111, add 1, the carry falls off the top, and you land back on 0000 0000. One zero. Problem solved.
Now watch the payoff. Compute 5 + (-3) with the plain binary adder, no special cases. That is 0000 0101 + 1111 1101. Add them column by column and the result is 1 0000 0010; the leading 1 is a carry that falls off the 8-bit edge and is discarded, leaving 0000 0010, which is +2. Correct, with the exact same adder you would use for unsigned numbers. This is the whole reason two's complement won: one circuit, one algorithm, both signs. The CPU's arithmetic unit genuinely does not know or care whether you meant the operands as signed or unsigned — it just adds bit patterns.
negate(x) = (~x) + 1 # flip all bits, then add one
+5 : 0000 0101
~5 : 1111 1010 # flip
-5 : 1111 1011 # + 1
5 + (-3) in one plain adder:
0000 0101 (+5)
+ 1111 1101 (-3)
-----------
1 0000 0010 -> drop the carry off the 8-bit edge
0000 0010 = +2 correctThere is an even cleaner way to think about the same bytes, which is worth carrying around: in an n-bit two's-complement number the top bit is not a mere sign flag — it carries a negative place value. For a byte the bit weights are +128 for none... wait, precisely: the leftmost weight is -128, and the rest are the usual +64, +32, ..., +1. So 1111 1011 reads as -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = -5 directly, no flipping needed. That single negative-weight view is the cleanest mental model, and it makes the next idea — the sign bit — obvious.
The sign bit, the asymmetric range, and why -1 is all ones
From the negative-weight view, the leftmost bit earns its name as the sign bit honestly: if it is 0 the number is zero or positive, if it is 1 the number is negative — because only that bit contributes a negative amount. That same top bit is the most significant bit (the biggest place value, MSB); the rightmost is the least significant bit (worth 1, LSB). So a quick read of any two's-complement byte is: glance at the MSB to learn the sign, then weigh the rest. 1111 1111 has its sign bit set, so it is negative — and computing it gives -128 + 127 = -1. That is why -1 in any width is 'all ones': 0xFF in a byte, 0xFFFFFFFF in a 32-bit int.
There is one genuine asymmetry you must respect, not gloss over. An 8-bit signed value runs from -128 to +127, not -127 to +127. The negative side reaches one further than the positive side because zero lives on the positive team, using up one of the 'non-negative' patterns. The honest consequence: the most negative number has no positive partner. In a byte you can write +127 but you cannot write +128, and crucially `-(-128)` overflows right back to -128, because flip-and-add-1 on 1000 0000 gives 1000 0000 again. Treat that single value as a permanent edge case in any code that negates or takes absolute values.
Two's complement also makes widening clean. Storing a small negative value into a wider box — a `signed char` into an `int`, say — must preserve its meaning, so the new high bits are filled with copies of the sign bit, a move called sign extension. The byte -5 = 1111 1011 becomes the 32-bit 0xFFFFFFFB, still -5. (Unsigned widening instead pads with 0s, called zero extension.) The fixed widths themselves — 8, 16, 32, 64 — are named precisely by the fixed-width types like int8_t and uint32_t, which spell out both the size and the sign so you are never guessing.
Where this bites you in real C
All of this stops being trivia the moment you mix signs in C. The choice between signed and unsigned types changes how the same bits are read, and the language has a habit of converting silently. Compare a `signed int` holding -1 against an `unsigned int` holding 1, and C promotes the -1 to unsigned first: its bit pattern 0xFFFFFFFF is reread as the huge unsigned value 4294967295, so `-1 > 1u` comes out true. The bits never moved; only the lens changed, exactly as the second section warned.
These conversions follow real, fixed rules under integer promotion and conversion — they are not random — but they are easy to forget under deadline. A classic bug: `for (size_t i = n - 1; i >= 0; i--)` never stops, because `size_t` is unsigned, so `i >= 0` is always true and `i--` from 0 wraps around to a gigantic number instead of going negative. The fix is to keep loop counters that go below zero in a signed type, or restructure the loop. The lesson is not 'avoid unsigned' but 'know which lens each value wears, and watch the boundary where they meet'.
One honest caveat to close on. Two's complement is now the law of the land — since the C23 standard it is the only signed representation C allows, and every mainstream CPU has used it for decades — so you can rely on the bit-level facts above for unsigned arithmetic and for reading patterns. But do not conclude that signed arithmetic is safe to overflow. Signed overflow is still undefined behavior in C, a separate trap we tackle in the very next guide on overflow and wraparound. Knowing the representation is not the same as being allowed to walk off its edge.