Two's Complement: How Computers Represent Negative Numbers
In computer architecture, 2’s complement is the standard method for representing signed integers (both positive and negative numbers). It’s elegant because it allows the CPU to perform subtraction using the exact same hardware logic as addition.
The Core Concept
In a fixed-width binary system (like 8-bit or 16-bit), the most significant bit (MSB)—the bit furthest to the left—acts as the sign bit:
| MSB Value | Number Sign |
|---|---|
| 0 | Positive |
| 1 | Negative |
This means the same bit pattern can represent different values depending on whether we interpret it as signed or unsigned.
Example: The Byte 11111111
| Interpretation | Value |
|---|---|
| Unsigned | 255 |
| Signed (8-bit) | -1 |
Identifying the Sign of a Number
Whether a binary number is positive or negative depends entirely on how the computer is interpreting that data.
1. Data Type Context
| Type | MSB Meaning |
|---|---|
| Unsigned | MSB is part of the magnitude (always positive) |
| Signed | MSB indicates the sign |
2. Fixed Width Matters
You must know the bit-width to know which bit is the “leftmost” one.
10000001 as 8-bit signed: -127
10000001 as 16-bit signed: +129 (padded as 00000000 10000001)
C Language Data Types
In C, how you declare a variable determines how the CPU treats the MSB.
| Type | Range | Description |
|---|---|---|
signed char |
-128 to 127 | 8-bit, MSB is sign |
unsigned char |
0 to 255 | 8-bit, all magnitude |
signed int |
-2,147,483,648 to 2,147,483,647 | 32-bit, MSB is sign |
unsigned int |
0 to 4,294,967,295 | 32-bit, all magnitude |
The “Wrap Around” Effect
Because 2’s complement uses one bit for the sign, the maximum positive value is roughly half that of an unsigned variable. If you have a signed char at 127 (01111111) and add 1:
01111111 (127)
+ 1
─────────
10000000 (-128 in signed, 128 in unsigned)
The CPU interprets 10000000 as -128 in signed context!
Converting to 2’s Complement
To represent a negative number (e.g., converting +5 to -5):
Step-by-Step: 5 → -5
| Step | Action | Result |
|---|---|---|
| 1 | Start with positive binary | 00000101 |
| 2 | Invert all bits (1’s complement) | 11111010 |
| 3 | Add 1 | 11111011 |
Final: 11111011 represents -5 in 8-bit 2’s complement.
Why This Works
The inversion and addition effectively calculates 2⁸ - 5 = 256 - 5 = 251, which is 11111011 in binary. When interpreted as signed, this equals -5.
Reading Negative 2’s Complement Values
To find the decimal value of a negative 2’s complement number, use a weighted sum where the MSB has a negative weight.
For an 8-bit number, the MSB weight is -128.
Example: Reading 11111011
Position: -128 64 32 16 8 4 2 1
Binary: 1 1 1 1 1 0 1 1
Weighted: -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1
= -128 + 123 = -5
Calculation:
(-128 × 1) + (64 × 1) + (32 × 1) + (16 × 1) + (8 × 1) + (4 × 0) + (2 × 1) + (1 × 1)
= -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1
= -128 + 123
= -5
Why Computers Use This System
1. One Zero
There’s only one representation for zero (00000000). Other signed number representations have both +0 and -0, which complicates hardware.
2. Unified Arithmetic
To subtract B from A, the computer calculates A + (-B). This means:
- A single “adder” circuit handles both addition and subtraction
- No separate subtraction hardware needed
- Simpler CPU design
Example: 7 - 5
The CPU calculates: 7 + (-5)
00000111 (7)
+ 11111011 (-5 in 2's complement)
─────────
00000010 (2) ← The carry bit is discarded!
Practical Examples
C: Signed vs Unsigned
#include <stdio.h>
int main() {
unsigned char u_val = 255; // 11111111 in binary
signed char s_val = 255; // Also 11111111 in binary
// u_val treats the MSB as 128, total 255
printf("Unsigned char: %u\n", u_val); // Output: 255
// s_val treats the MSB as -128, total -1
printf("Signed char: %d\n", s_val); // Output: -1
return 0;
}
Compile and run:
gcc limits.c -o limits
./limits
Python: Simulating 8-bit Behavior
Python normally has arbitrary precision, so we need to manually implement 2’s complement logic:
def interpret_as_8bit_signed(byte_val):
"""
Interpret an 8-bit value as signed.
If the MSB (bit 7) is set, the value is negative.
"""
# Check if the 8th bit (MSB) is set using bitwise AND
if byte_val & 0x80:
# It's negative: subtract 2^8 (256) to get the 2's complement value
return byte_val - 256
else:
# It's positive
return byte_val
# The binary pattern 11111111 (255 in decimal)
binary_pattern = 0b11111111
print(f"Binary pattern: {bin(binary_pattern)}")
print(f"Interpreted as Unsigned: {binary_pattern}")
print(f"Interpreted as Signed: {interpret_as_8bit_signed(binary_pattern)}")
Output:
Binary pattern: 0b11111111
Interpreted as Unsigned: 255
Interpreted as Signed: -1
Quick Reference Table
| Binary (8-bit) | Unsigned | Signed |
|---|---|---|
00000000 |
0 | 0 |
00000001 |
1 | 1 |
01111111 |
127 | 127 |
10000000 |
128 | -128 |
10000001 |
129 | -127 |
11111110 |
254 | -2 |
11111111 |
255 | -1 |
Key Takeaways
| Concept | Rule |
|---|---|
| MSB = 0 | Number is positive |
| MSB = 1, unsigned | Number is positive (large) |
| MSB = 1, signed | Number is negative |
| Convert to negative | Invert bits, add 1 |
| Read negative | MSB has negative weight (-128 for 8-bit) |
| Zero | Only one representation: all zeros |
Practice Exercise
Try converting these yourself:
-
+8 to -8 in 8-bit 2’s complement
- Start:
00001000 - Invert:
11110111 - Add 1:
11111000
- Start:
-
What decimal value is
10110000as signed?- MSB is 1 → negative
- Weighted sum: -128 + 32 + 16 = -80