Two's Complement

So far every binary number you have met has been positive — the columns are worth 1, 2, 4, 8, \dots and you add up the ones. But a real computer must also store negative integers: temperatures below zero, an account overdrawn, a sprite moving left. It has no minus-sign key and no extra symbols — only 0s and 1s, packed into a fixed width such as 8 bits. So how do you fit a minus sign into a byte?

The answer is a beautifully sneaky code called two's complement. It is the scheme used by essentially every processor on Earth, and its killer feature is this: once negatives are stored this way, ordinary binary addition — the exact same binary addition you already know, with the same adder circuit — gives the right answer for subtraction too. No separate subtracting hardware required. That single trick is why two's complement won.

The big idea: make the top column negative

In an ordinary 8-bit number the columns are 128, 64, 32, 16, 8, 4, 2, 1 — all positive. Two's complement changes one thing: the leftmost column (the most significant bit, or MSB) is given a negative place value, -128. Every other column stays exactly as it was.

Read a signed byte the same way as always — add up the columns that hold a 1 — but remember the top one subtracts 128. Because that top bit is the only one that can make the total negative, it doubles as a sign flag: a leading 0 means the number is zero-or-positive, a leading 1 means it is negative.

Reading a negative byte

Take 11110011. The top bit is 1, so this is negative. Add up the on-columns, remembering the sign of the first:

-128 + 64 + 32 + 16 + 0 + 0 + 2 + 1 = -13.

And 00001101? The top bit is 0, so it is positive: 8 + 4 + 1 = +13. Notice how those two bytes look — they are mirror-ish images of one another. That is not a coincidence; it is exactly the negation rule we meet next.

Negating a number: invert, then add one

To find the two's-complement pattern for -x when you already have x, follow one short recipe:

Let's turn +13 = 00001101 into -13. Watch the two steps run:

Out comes 11110011 — precisely the byte we decoded as -13 above. The lovely part is that the recipe is its own inverse: invert and add one to 11110011 and you land back on 00001101. The same button both negates and un-negates.

Inverting a byte replaces x with 11111111 - x, because flipping every bit is the same as subtracting from the all-ones pattern. Now 11111111 is just 256 - 1 = 255 in 8 bits, so the inverse is 255 - x. Add the 1 and you have 256 - x. On an 8-bit wheel of 256 values, 256 - x and -x sit on the very same spot (they differ by a full lap of 256). That is the whole magic — two's complement is arithmetic modulo 2^n, and the negative numbers are simply the far side of the wheel.

Why bother? Subtraction becomes free addition

Here is the pay-off. To compute a - b, a processor does not subtract. It negates b (invert-and-add-one) and then adds, throwing away any carry that runs off the top. Let's do 20 - 13 in a byte.

+20 = 00010100, and we found -13 = 11110011. Add them:

00010100 + 11110011 = 1\,00000111.

The answer needs a ninth bit, but we only keep 8: drop the leading carry and we are left with 00000111 = +7. And 20 - 13 = 7. It just works — no borrow, no special circuit, only the adder you already have. Try it live below.

Convert and negate yourself — edit and Run

This program stores signed values in 8 bits. It prints the bit pattern for a number, negates it by inverting and adding one, and decodes patterns back to signed values. Change x and press Run.

const BITS = 8; const MASK = (1 << BITS) - 1; // 11111111 = 255 // Encode a signed number as an 8-bit two's-complement string. function toByte(n: number): string { return ((n & MASK) >>> 0).toString(2).padStart(BITS, "0"); } // Decode an 8-bit pattern back to its signed value (top bit worth -128). function fromByte(bits: string): number { const u = parseInt(bits, 2); return u >= 128 ? u - 256 : u; } // Negate by inverting every bit and adding one. function negate(bits: string): string { const inverted = (~parseInt(bits, 2) & MASK) >>> 0; return toByte(inverted + 1); } const x: number = 13; const bx: string = toByte(x); const bneg: string = negate(bx); console.log(x + " = " + bx); console.log("invert + 1 -> " + bneg + " = " + fromByte(bneg)); // Subtraction as addition: 20 - 13 = 20 + (-13) const sum = (parseInt(toByte(20), 2) + parseInt(bneg, 2)) & MASK; console.log("20 + (-13) = " + toByte(sum) + " = " + fromByte(toByte(sum)));

Watch the pitfalls

The single most common mistake is to read a signed byte like an unsigned one. In unsigned binary, 11110011 is 128+64+32+16+2+1 = 243. In two's complement the very same bits mean -13, because that leading 1 is worth -128, not +128. A leading 1 always signals a negative number. The bits alone never tell you which — you must know whether the value is being treated as signed or unsigned.

A signed byte only reaches +127. Add +100 + 50: 01100100 + 00110010 = 10010110. That top bit is now 1, so the stored result reads as -106 — the answer 150 was too big to fit and has overflowed into the sign, coming out negative. The tell-tale sign of overflow in two's complement is adding two numbers of the same sign and getting an answer of the opposite sign. There is no error message; the wrong number just sails on. Processors set an overflow flag for exactly this, but if your program never checks it, the bug is silent — always ask whether the largest possible result still fits the width.

An older scheme, sign-and-magnitude, used the top bit purely as a plus/minus flag and the rest as size — which gives you both a +0 (00000000) and a -0 (10000000), two zeros for one number, and it breaks ordinary addition. Two's complement has a single zero (00000000; invert-and-add-one on it wraps neatly back to itself) and one spare negative pattern 10000000 = -128 with no positive twin. That single zero and the free subtraction are why every modern CPU chose it.