How computers represent signed integers using two's complement — why -x = ~x + 1, the asymmetric range, sign extension, and why this system is universal in modern computing.
Two's complement is the standard method for representing signed integers in virtually every modern computer. It was adopted because it lets the same hardware adder circuit handle both signed and unsigned addition without any special logic. When I first learned this in my computer architecture class, the elegance of it struck me: subtraction becomes addition of a negative number, and the hardware does not need to care about sign.
In two's complement, the most significant bit (MSB) serves as the sign bit. If the MSB is 0, the number is positive or zero. If it is 1, the number is negative. But unlike older systems like sign-magnitude or one's complement, the weight of the MSB is negative: in an 8-bit two's complement number, the MSB represents -128 rather than +128.
I struggled with two's complement in college until I wrote a small program that showed the binary representation of negative numbers. Seeing that -1 is all 1s and -128 is 10000000 in 8-bit finally made it click for me.
# 8-bit two's complement examples # Positive numbers: same as unsigned binary 0 = 0000 0000 1 = 0000 0001 7 = 0000 0111 63 = 0011 1111 127 = 0111 1111 # largest positive # Negative numbers: MSB is 1 -128 = 1000 0000 # most negative -1 = 1111 1111 # all bits set -5 = 1111 1011 -32 = 1110 0000 # Verify: (-5) + 5 = 0 1111 1011 (-5) + 0000 0101 (+5) = 0000 0000 (0, with carry-out discarded)
The property that makes two's complement work is that adding any number and its two's complement negative gives zero, with the carry out of the high bit being discarded. This is automatic in fixed-width arithmetic — the CPU does not need to check signs.
The defining equation of two's complement is -x = ~x + 1. To negate any number, you flip all its bits (one's complement) and then add 1. This works for both positive and negative numbers — applying the transformation twice yields the original value.
# Negating 5 in 8-bit two's complement x = 5 = 0000 0101 ~x = 1111 1010 (one's complement) ~x + 1 = 1111 1011 = -5 ✓ # Applying twice restores the original y = -5 = 1111 1011 ~y = 0000 0100 ~y + 1 = 0000 0101 = 5 ✓ # Negating 0 (the edge case) x = 0 = 0000 0000 ~x = 1111 1111 ~x + 1 = 0000 0000 (overflow discarded) = 0 ✓ # Negating -128 (the edge case — asymmetric!) x = -128 = 1000 0000 ~x = 0111 1111 ~x + 1 = 1000 0000 = -128 (!) # This is the one case where negation returns itself due to overflow
The last example reveals the asymmetric range of two's complement: there is one more negative value than positive. In 8-bit, you can represent -128 but not +128. The negation of -128 overflows back to -128. This is not a bug in the system — it is a consequence of having an even number of bit patterns and reserving zero for the positive side.
The mathematical intuition: in n-bit unsigned arithmetic, ~x = (2^n - 1) - x (the one's complement). Adding 1 gives 2^n - x. Since 2^n wraps to 0 in n-bit arithmetic, -x and 2^n - x are congruent modulo 2^n. This is why the same adder circuit works for both signed and unsigned operations.
The range of an n-bit two's complement number is asymmetric by design. There are 2^n distinct bit patterns. Zero occupies one pattern on the positive side, so the negatives get the remaining 2^(n-1) patterns while the positives get 2^(n-1)-1.
| Bit Width | Range | Zero Pattern | All Ones |
|---|---|---|---|
| 8-bit | -128 to 127 | 0000 0000 | -1 (255 unsigned) |
| 16-bit | -32,768 to 32,767 | 0000 0000 0000 0000 | -1 (65,535 unsigned) |
| 32-bit | -2,147,483,648 to 2,147,483,647 | 0x00000000 | 0xFFFFFFFF (-1) |
| 64-bit | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | 0x0000000000000000 | 0xFFFFFFFFFFFFFFFF (-1) |
This asymmetry is relevant in practice. The absolute value of the minimum negative number cannot be represented as a positive number in the same width. In C, -INT32_MIN is undefined behavior. In Java, Math.abs(Integer.MIN_VALUE) returns Integer.MIN_VALUE — a well-known quirk that I have seen cause subtle bugs in production code.
// The MIN_VALUE gotcha (Java example) int min = Integer.MIN_VALUE; // -2147483648 int abs = Math.abs(min); // -2147483648 (!) // There is no +2147483648 in 32-bit two's complement // Safe absolute value using long long safeAbs = Math.abs((long) min); // 2147483648 // C example — undefined behavior int x = INT32_MIN; int y = -x; // UNDEFINED BEHAVIOR: -(-2147483648) overflows 32-bit int
When you widen a two's complement number to more bits (e.g., from 8-bit to 16-bit or from 32-bit to 64-bit), you must preserve the sign. This is done by sign extension: copying the sign bit into all the new higher-order bit positions. For positive numbers, this means filling with zeros. For negative numbers, it means filling with ones.
# Sign extension from 8-bit to 16-bit # Positive: pad with zeros 8-bit: 0001 1010 (+26) 16-bit: 0000 0000 0001 1010 (+26) ✓ # Negative: pad with ones 8-bit: 1111 0110 (-10) 16-bit: 1111 1111 1111 0110 (-10) ✓ # What happens without sign extension (wrong): 8-bit: 1111 0110 (-10) 16-bit: 0000 0000 1111 0110 (+246) ✗ # In C, casting to a wider type does sign extension automatically int8_t a = -10; // 8-bit signed int16_t b = a; // 16-bit signed — sign extended automatically uint16_t c = (uint16_t)a; // ALSO sign-extended! c = 0xFFF6, not 0x00F6 // To get zero extension, cast through unsigned first uint16_t d = (uint16_t)(uint8_t)a; // d = 0x00F6 (246)
Sign extension happens implicitly in most languages when you assign a smaller integer type to a larger one. The confusing part — and I have debugged this more than once — is that the implicit conversion also happens when casting a signed value to an unsigned wider type. In C, (uint16_t)(int8_t)(-10) is 0xFFF6, not 0x00F6. The sign bit propagates before the unsigned interpretation kicks in.
When you need zero extension (treating the value as unsigned during widening), first cast to the unsigned version of the original width, then allow implicit widening. This is why (uint32_t)(uint8_t)(-1) gives 255, while (uint32_t)(int8_t)(-1) gives 4294967295. Our 32-bit programmer calculator lets you toggle between signed and unsigned interpretations to see the difference live.
Enter any negative number in our bitwise calculator and watch the two's complement binary representation update in real time. Toggle between signed and unsigned views to understand how the same bit pattern can represent different values.
Two's complement is the standard way computers represent signed integers. In this system, a negative number is created by inverting all bits of the positive number (one's complement) and then adding 1. The most significant bit acts as the sign bit: 0 for positive or zero, 1 for negative. This representation allows addition and subtraction to work identically on signed and unsigned values using the same hardware circuitry.
The formula -x = ~x + 1 is the mathematical definition of two's complement. Inverting all bits of x gives its one's complement, which is (2^n - 1) - x. Adding 1 gives 2^n - x, which is the two's complement representation of -x. For example, for x=5 in 8-bit: ~5 = 11111010, add 1 = 11111011 = -5. If you add x and its two's complement, you get 2^n, which overflows to 0 — proving it works.
An n-bit two's complement number can represent values from -2^(n-1) to 2^(n-1)-1. For example: 8-bit range is -128 to 127, 16-bit range is -32768 to 32767, 32-bit range is -2147483648 to 2147483647, and 64-bit range is -9223372036854775808 to 9223372036854775807. There is always one more negative value than positive value because zero occupies one positive slot.
Sign extension preserves the value when widening a two's complement number to more bits. The sign bit (the most significant bit) is copied into all new higher-order bit positions. For positive numbers, this means filling with zeros. For negative numbers, it means filling with ones. For example, extending -3 from 4-bit (1101) to 8-bit gives 11111101, which is still -3.
In Java, Integer.MIN_VALUE is -2147483648 (0x80000000). Its two's complement negation would be +2147483648, but that value cannot be represented in a 32-bit signed integer because the maximum positive value is 2147483647. The negation overflows and returns the original value. This is a well-known asymmetry of two's complement: the most negative number has no positive counterpart in the same bit width.