Computers store integers as fixed-width patterns of bits. Unsigned integers are easy: an 8-bit value runs from 00000000 (0) to 11111111 (255). Negative numbers need a convention, and nearly every processor made in the last fifty years uses the same one: two’s complement. Since C23 and C++20, it is the only representation those language standards allow.
The Rule in One Line
In an n-bit two’s complement number, the most significant bit has a negative weight of −2^(n−1). Every other bit keeps its normal positive weight.
For 8 bits, the place values are:
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Weight | −128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
So 11010001 is −128 + 64 + 16 + 1 = −47, and 01111111 is 127. The range of an 8-bit signed integer is therefore −128 to 127. In general, n bits cover −2^(n−1) to 2^(n−1) − 1.
How to Negate a Number
To find −x, invert every bit and add 1:
46 = 00101110
~46 = 11010001 (invert)
~46 + 1 = 11010010 (add one) = −46
Check it with the weights: −128 + 64 + 16 + 2 = −46. A popular shortcut: copy bits from the right up to and including the first 1, then invert everything to its left. 00101110 keeps 10 and inverts 001011 to 110100, giving 11010010.
Rearranging −x = ~x + 1 gives the identity that confuses many programmers when they first use the NOT operator:
~x = −x − 1
That is why ~5 is −6 in Python, JavaScript, Java, and C. You can see both views in the Bitwise NOT Calculator by switching on signed mode.
Why Two’s Complement Won
Older machines used sign-magnitude (one bit for the sign, the rest for the size) or one’s complement (negate by inverting). Both had two zeros, +0 and −0, and both needed special hardware for subtraction. Two’s complement fixes both problems:
- One zero.
00000000is the only zero, and the extra pattern becomes−128. - Addition just works. The same adder circuit handles signed and unsigned numbers.
−46 + 46in binary is11010010 + 00101110 = 1_00000000, and dropping the carry leaves00000000. - Subtraction is addition.
a − bis computed asa + (~b + 1).
Think of the 256 patterns as a clock face: counting up from 127 (01111111) wraps to −128 (10000000), just as counting up from 255 wraps to 0 for unsigned values.
Converting by Hand
Signed decimal to binary (8-bit):
- If the number is non-negative, convert normally and pad to 8 bits.
- If it is negative, convert its absolute value, then invert and add 1.
Example, −20: 20 = 00010100 → invert 11101011 → add 1 → 11101100.
Binary to signed decimal:
- If the top bit is
0, read it as a normal unsigned number. - If the top bit is
1, subtract2^nfrom its unsigned value.
Example: 11101100 is 236 unsigned, and 236 − 256 = −20.
Overflow
Signed overflow happens when a result doesn’t fit the range. In 8 bits, 100 + 50 should be 150, but:
01100100 (100)
+ 00110010 ( 50)
--------
10010110 (−106 signed, 150 unsigned)
Two positive inputs produced a negative-looking result. The rule for detecting it is simple: overflow happened if both inputs have the same sign and the result has a different sign.
How languages respond varies a lot:
- C and C++: signed overflow is undefined behaviour. Compilers may assume it never happens.
- Java and C#: results wrap around silently (C# can check with
checked). - Rust: panics in debug builds and wraps in release builds, with explicit
wrapping_addandchecked_addmethods. - Python: integers never overflow because they grow as needed.
Sign Extension
When a signed value moves into a wider type, its sign bit is copied into the new high bits. This is sign extension:
8-bit −20 = 11101100
16-bit −20 = 11111111 11101100
Zero-extending instead would produce 00000000 11101100, which is 236. This is exactly the difference between arithmetic and logical right shift, covered in Logical vs Arithmetic Right Shift.
Two’s Complement in Code
def to_signed(value: int, bits: int) -> int:
"""Interpret an unsigned bit pattern as two's complement."""
if value & (1 << (bits - 1)):
return value - (1 << bits)
return value
def to_unsigned(value: int, bits: int) -> int:
return value & ((1 << bits) - 1)
to_signed(0b11101100, 8) # -20
to_unsigned(-20, 8) # 236
// Interpret a byte as signed using typed arrays
const buf = new Int8Array([236]);
console.log(buf[0]); // -20
console.log(-20 & 0xFF); // 236
console.log(-1 >>> 0); // 4294967295
Key Takeaways
- The top bit carries a negative weight, so
nbits hold−2^(n−1)to2^(n−1) − 1. - Negate with “invert and add one”. As a result,
~x = −(x + 1). - The same hardware adds signed and unsigned numbers. Only the interpretation differs.
- Width matters. Always know whether you’re looking at 8, 16, 32, or 64 bits. The Binary Bitwise Calculator lets you fix the width and see every bit.