Two's Complement: How Computers Represent Negative Numbers
How two's complement encodes negative integers, why CPUs run signed and unsigned math on one adder, and the edge cases — INT_MIN, overflow, sign extension — that cause real bugs.
Type int8_t x = -1; and the byte sitting in memory is 0xFF — all eight bits set to 1. That looks nothing like the number negative one, yet every mainstream CPU agrees it is. The reason is two’s complement, the encoding almost every machine built since the 1970s uses to store signed integers. Understanding it explains a long list of otherwise baffling behavior: why INT_MIN has no positive twin, why abs() can return a negative number, and why subtraction and addition run on the exact same circuit.
Why a plain sign bit fails
The obvious way to store a negative number is to steal the top bit as a sign flag and use the rest for magnitude. In 8 bits, 0000_0101 is +5 and 1000_0101 is -5. This is called sign-magnitude, and it has two problems that killed it for general-purpose hardware.
First, there are two zeros. 0000_0000 is +0 and 1000_0000 is -0. Two bit patterns for the same value means every equality check has to special-case it, and you waste a representable slot.
Second, arithmetic needs a branch. To add a positive and a negative number, the hardware has to compare magnitudes, subtract the smaller from the larger, and choose the sign of the result. That is a conditional path in silicon, which is slower and larger than a path with no branch.
One’s complement (negate by flipping every bit) removes the branch partway but still carries two zeros (0000_0000 and 1111_1111) and needs an end-around carry. Two’s complement drops both warts.
How two’s complement actually works
The rule for negating a number is two steps: invert every bit, then add 1.
Take +5 in 8 bits: 0000_0101. Invert: 1111_1010. Add one: 1111_1011. That is -5. Run the same procedure on 1111_1011 and you land back on 0000_0101 — the operation is its own inverse, which is exactly what negation should be.
The clean way to read a two’s complement number is to give the top bit a negative weight. In an 8-bit value, bit 7 is worth -128 instead of +128; every other bit keeps its normal positive weight. So 1111_1011 is -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5. The same formula reads 0111_1111 as 127 and 1000_0000 as -128.
That gives an N-bit range of -2^(N-1) to 2^(N-1) - 1. For 8 bits that is -128 to 127. For 32-bit int, it is -2,147,483,648 to 2,147,483,647.
The payoff is that addition does not care about sign at all. Compute -5 + 5 as 1111_1011 + 0000_0101. The sum is 1_0000_0000; the carry out of bit 7 is discarded, leaving 0000_0000 = 0. One adder handles signed and unsigned values, and subtraction is just “negate the second operand and add.” That single-circuit property is the entire reason the encoding won.
The edge cases that bite you
The range is asymmetric: one more negative number than positive. In 8 bits, +127 has a counterpart at -127, but -128 has nobody. Run the negate procedure on -128 (1000_0000): invert to 0111_1111, add one, and you get 1000_0000 again — still -128. So -INT_MIN == INT_MIN, and abs(INT_MIN) is undefined behavior in C because the true answer is not representable. This is a genuine source of crashes in code that negates user-supplied integers without checking.
Overflow has a precise signature. When you add two numbers with the same sign and the result comes out with the opposite sign, the value wrapped. 0111_1111 (127) + 0000_0001 (1) = 1000_0000, which reads as -128. Two positives produced a negative, so the hardware sets the overflow flag. CPUs detect this by checking whether the carry into the sign bit differs from the carry out of it.
Sign extension is the other place people slip. Widening an 8-bit value to 16 bits means copying the sign bit into all the new high bits, not zero-filling them. -5 as 1111_1011 becomes 1111_1111_1111_1011, which is still -5. Zero-filling would turn it into 251. This is why C distinguishes arithmetic shift right (fills with the sign bit) from logical shift right (fills with zero), and why mixing signed and unsigned types in a comparison can silently flip a result.
Here is how the three schemes line up for a single byte:
| Pattern | Sign-magnitude | One’s complement | Two’s complement |
|---|---|---|---|
0000_0000 | +0 | +0 | 0 |
1000_0000 | -0 | -127 | -128 |
1111_1111 | -127 | -0 | -1 |
| Zeros | two | two | one |
| Add needs branch | yes | end-around carry | no |
Cursor
When you're poking at bit-level behavior, an editor that can explain a hex dump or scaffold a quick test harness shortens the loop. Cursor's inline chat is useful for sanity-checking shift and mask logic against what the standard actually guarantees.
Free tier; Pro $20/mo
Affiliate link · We earn a commission at no cost to you.
The encoding is old and unglamorous, but it sits under every integer your code touches. Once the negative-weight-on-the-top-bit model clicks, the surprises stop being surprises.
FAQ
Why is it called two's complement?+
Do all computers use two's complement?+
How do I safely negate a value that might be INT_MIN?+
Related reading
2026-06-10
LSM-Trees vs B-Trees: The Write-Optimized Database Tradeoff
Why some databases append writes and reconcile later while others edit in place — and how that one choice shapes write throughput, read latency, and disk usage.
2026-06-10
Copy-on-Write, Explained Through fork() and Snapshots
How copy-on-write defers copying until a write actually happens — the mechanism behind fast fork(), filesystem snapshots, and database MVCC, explained with page tables and page faults.
2026-06-10
A Coroutine Is Not a Thread: What Suspends, What Gets Scheduled, and Why It Matters
A coroutine suspends and resumes cooperatively; a thread is preempted by the OS. Here is the real difference in scheduling, memory, and parallelism — and when each one wins.
2026-06-10
What MVCC Is, and How Databases Let Readers and Writers Coexist
MVCC keeps multiple versions of every row so reads never block writes. Here's how Postgres implements it with xmin/xmax, why your tables bloat, and where snapshot isolation bites.
2026-06-09
What a Merkle Tree Is, and Where You've Already Seen One
A Merkle tree hashes data into a single fingerprint so you can verify any piece without downloading the whole set. Here's how it works and where it already runs in your stack.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.