How signed integers are represented in binary — why -1 is all 1s and how overflow works
Two's complement is the most common method for representing signed integers in binary across virtually all modern computer architectures. In my programming career spanning embedded systems, Java enterprise applications, and everything in between, I have never encountered a system that did not use two's complement for signed integers. It was standardized because it allows the same hardware to add, subtract, and compare both signed and unsigned numbers using identical circuitry.
The brilliance of two's complement lies in its simplicity: the most significant bit (MSB) acts as a sign bit with a negative weight. For an 8-bit number, the bits represent -128, 64, 32, 16, 8, 4, 2, and 1. If the MSB is 1, you add -128 to the sum of the positive bits. This means that 11111111 = -128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1. That is why -1 is all 1s in binary — it is simply -128 + 127 = -1.
Key Insight: In two's complement, subtraction is implemented as addition. Computing A - B is the same as A + (~B + 1). The CPU does not need a separate subtraction circuit — it just negates the subtrahend using two's complement and adds.
To negate a number in two's complement, you flip all the bits and add 1. This is the process that every CPU uses internally when you write something like result = -x. Let me walk through the exact steps:
// C: visualizing two's complement negation
#include <stdio.h>
int main() {
signed char x = 5; // binary: 00000101
// Step 1: flip all bits (ones' complement)
signed char flipped = ~x; // binary: 11111010
// Step 2: add 1
signed char negated = flipped + 1; // binary: 11111011 = -5
printf("x = %d\n", x); // 5
printf("~x = %d\n", flipped); // -6 (which is ~x in two's complement)
printf("-x = %d\n", negated); // -5 (correct!)
// The formula: -x = ~x + 1
// Check: for x = 5: ~5 = -6, -6 + 1 = -5 ✓
return 0;
}
# Python: illustrating the same
x = 5
print(f"x = {x}, binary: {bin(x)}")
print(f"~x = {~x}") # -6 (NOT flips ALL bits including infinite leading zeros)
print(f"-x = {-x}") # -5
One thing I've noticed is that beginners often confuse the bitwise NOT operator (~) with negation (-). In two's complement, ~x gives you -(x+1), not -x. To get -x, you need ~x + 1. This is a fundamental property of two's complement encoding that I wish had been explained to me on day one of learning C.
This is the most common question I get from junior developers, and it is an excellent one. Let me explain with a full 8-bit example. In an 8-bit signed integer, the value ranges from -128 to 127. The number -1 is represented as 0xFF (all bits set to 1):
// Java: demonstrating -1 in binary
public class TwosComplement {
public static void main(String[] args) {
byte b = -1;
// Print as unsigned int to see the full 8-bit pattern
System.out.println(String.format("0x%02X", b & 0xFF)); // 0xFF
// Why? Because -1 = 256 - 1 = 0xFF in 8-bit two's complement
// More generally, for N-bit two's complement:
// -1 = 2^N - 1 = all bits set to 1
// Let's verify the math:
// -128 + 127 = -1 (using the bit weights: MSB = -128, rest sum to 127)
byte minusOne = (byte)0b11111111;
System.out.println(minusOne); // -1
// Comparing with -128 (0x80)
byte minValue = (byte)0b10000000;
System.out.println(minValue); // -128
}
}
Here is how I explain it to new programmers: imagine a car odometer that goes from 0 to 999999. If the odometer reads 000000 and you drive backward 1 mile, it wraps around to 999999. In two's complement, 999999 is the representation of -1. The same principle applies in binary: 00000000 minus 1 wraps to 11111111, which is -1 in two's complement.
The range difference between signed and unsigned integers of the same bit width is a critical concept that I see causing bugs all the time. Here is how the ranges break down:
// C: understanding signed vs. unsigned ranges
#include <stdint.h>
#include <stdio.h>
int main() {
// 8-bit types
uint8_t u8_min = 0;
uint8_t u8_max = 255; // 2^8 - 1
int8_t s8_min = -128; // -2^7
int8_t s8_max = 127; // 2^7 - 1
// 16-bit types
uint16_t u16_max = 65535; // 2^16 - 1
int16_t s16_min = -32768; // -2^15
int16_t s16_max = 32767; // 2^15 - 1
// 32-bit types
uint32_t u32_max = 4294967295U; // 2^32 - 1
int32_t s32_min = -2147483648; // -2^31
int32_t s32_max = 2147483647; // 2^31 - 1
// The general formula:
// Unsigned N-bit: [0, 2^N - 1]
// Signed N-bit: [-2^(N-1), 2^(N-1) - 1]
return 0;
}
# Python: Python integers have unlimited precision,
# so there is no fixed range and no overflow.
# But for simulation:
N = 8
signed_min = -(2**(N-1)) # -128
signed_max = 2**(N-1) - 1 # 127
unsigned_max = 2**N - 1 # 255
Note how the ranges are asymmetric: the negative range extends one further than the positive range. This is because zero occupies one of the positive bit patterns, so there are 2^(N-1) negative numbers but only 2^(N-1) - 1 positive numbers. In an 8-bit signed integer, you can represent -128 but not +128.
This is where two's complement truly shines. The same binary addition circuit handles both signed and unsigned arithmetic without any special casing. I remember building a simple 8-bit CPU simulator in college — implementing two's complement addition in hardware took just a few logic gates, and subtraction was literally free because it is just addition of the negated operand.
// C: two's complement addition — same hardware for signed and unsigned
#include <stdio.h>
#include <stdint.h>
int main() {
// Same bit pattern, different interpretation
uint8_t ua = 200, ub = 100;
int8_t sa = (int8_t)ua, sb = (int8_t)ub;
printf("unsigned: %u + %u = %u\n", ua, ub, ua + ub); // 300 % 256 = 44
printf("signed: %d + %d = %d\n", sa, sb, sa + sb); // -56 + 100 = 44
// Same result! The CPU performed identical operations.
// The interpretation is left to the programmer.
// Subtraction via addition: A - B = A + (~B + 1)
int a = 10, b = 3;
int subtract_hardware = a + (~b + 1); // 10 + (-3) = 7
printf("10 - 3 = %d (using two's complement)\n", subtract_hardware);
return 0;
}
One thing I've noticed is that this property makes two's complement uniquely suited for modern computing. The same ALU (Arithmetic Logic Unit) instruction handles both uint8_t addition and int8_t addition with zero modification. The programmer just decides how to interpret the resulting bit pattern. This is a beautiful example of mathematical elegance driving practical engineering decisions.
Overflow occurs when the result of an arithmetic operation exceeds the range of the data type. In two's complement, overflow detection requires checking the carry into and out of the sign bit. I've debugged my fair share of overflow bugs — they are notoriously hard to spot because the code compiles and runs without errors, just producing wrong results.
// Java: overflow detection in two's complement
public class OverflowDemo {
public static void main(String[] args) {
// Signed overflow: wrapping around
int max = Integer.MAX_VALUE; // 2147483647
int overflowed = max + 1; // -2147483648 !!
System.out.println("Max + 1 = " + overflowed);
// This is not random — it is correct two's complement wrapping
// 011111...111 + 1 = 100000...000 = Integer.MIN_VALUE
// Detecting overflow after addition
int a = 2000000000;
int b = 2000000000;
int sum = a + b;
// Overflow detection for signed addition
boolean overflow = (a > 0 && b > 0 && sum < 0)
|| (a < 0 && b < 0 && sum > 0);
System.out.println("Overflow detected: " + overflow);
// Java 8+ helper for exact arithmetic
try {
int safe = Math.addExact(a, b);
} catch (ArithmeticException e) {
System.out.println("Overflow via Math.addExact()");
}
}
}
// C: detecting overflow
#include <limits.h>
#include <stdio.h>
int main() {
int x = INT_MAX;
if (x + 1 < x) { // wraps to negative
printf("Overflow detected!\n");
}
return 0;
}
Overflow bugs are especially dangerous in security-critical code. Buffer overflow vulnerabilities often exploit integer overflow: a size calculation wraps around to a small value, the allocation is too small, and then data is copied past the end of the buffer. I always recommend using checked arithmetic (like Java's Math.addExact or C++'s checked_integer library) in security-sensitive contexts.
Sign extension is the process of converting a signed integer to a larger bit width while preserving its value. For two's complement numbers, this means replicating the sign bit into all the new higher-order bit positions. This is something I deal with regularly when reading binary file formats that use smaller integer types packed into larger fields.
// C: sign extension in action
#include <stdio.h>
#include <stdint.h>
int main() {
int8_t small = -16; // 8-bit: 11110000
int16_t extended = small; // 16-bit: 1111111111110000
printf("int8_t: %d\n", small); // -16
printf("int16_t: %d\n", extended); // -16 (correct! sign-extended)
// Without sign extension, -16 would become 240 (wrong!)
uint16_t wrong = (uint16_t)(uint8_t)small; // zero-extended
printf("Wrong (zero-extended): %u\n", wrong); // 65424 or 240
return 0;
}
// Java: sign extension is automatic for byte -> int
byte b = (byte)0xF0; // -16
int i = b; // -16 (automatic sign extension)
int j = b & 0xFF; // 240 (manual zero extension for unsigned interpretation)
In embedded systems programming, I frequently need to read multi-byte values from registers or memory-mapped I/O. A common bug is reading a signed byte from a device register and casting it to a 32-bit integer — if the byte is negative in two's complement, the sign extends and your 32-bit value is correct. But if you intended the value as unsigned, you need to mask it manually with & 0xFF to prevent sign extension.
Not all languages handle two's complement the same way. Here is what I've learned from working across multiple languages:
// Java: fixed-width integers, explicit two's complement
int x = -1;
String bits = Integer.toBinaryString(x); // "11111111111111111111111111111111"
// Integer.toBinaryString returns all 32 bits for int, 64 for long
# Python: unlimited precision, but bin() hides leading 1s
x = -1
bits = bin(x) # "-0b1" — Python shows the signed value, not the bit pattern
# To see the two's complement representation:
def twos_complement_bits(n, bits=32):
if n < 0:
n = (1 << bits) + n
return format(n, '0{}b'.format(bits))
print(twos_complement_bits(-1)) # "11111111111111111111111111111111"
// C: exact bit widths with stdint.h
int32_t x = -1;
// Cast to unsigned to print full bit pattern:
printf("0x%08X\n", (uint32_t)x); // 0xFFFFFFFF
Python's approach is a frequent source of confusion. When you call bin(-1), Python returns "-0b1" rather than the underlying bit pattern. This is because Python integers conceptually have infinite leading sign bits — negative numbers have infinite leading 1s, and positive numbers have infinite leading 0s. To see the actual two's complement representation for a fixed width, you must mask the value.
Let me share a real debugging story from my work on an embedded GPS tracking device. The device reported altitude as a signed 16-bit integer in two's complement. On good days, altitudes were positive (above sea level). But when the device was in a valley below sea level, the altitude was supposed to be negative.
// The buggy code (C):
uint16_t raw_altitude = read_register(ALTITUDE_REG);
int16_t altitude = (int16_t)raw_altitude;
printf("Altitude: %d meters\n", altitude);
// This looks correct — and it IS correct if the register
// stores two's complement signed values. But the hardware
// engineer had documented "unsigned 16-bit with 1000m offset".
// The correct code:
uint16_t raw = read_register(ALTITUDE_REG);
int16_t altitude = (int16_t)(raw - 1000);
printf("Altitude: %d meters\n", altitude);
The lesson I took from this: hardware documentation is not always accurate, and when debugging binary-level issues, the first thing to verify is whether a value is signed or unsigned, and what encoding it uses. A single misinterpreted sign bit can send your entire system's logic off the rails.
Two's complement simplifies hardware because addition and subtraction use the same circuitry regardless of sign. Sign-magnitude would require different logic for positive and negative operands. Two's complement also has only one representation for zero, while sign-magnitude has both +0 and -0, which wastes a bit pattern and complicates comparisons.
For N-bit two's complement, the smallest (most negative) number is -2^(N-1). For 8-bit: -128 (10000000). For 16-bit: -32768 (1000000000000000). For 32-bit: -2147483648. For 64-bit: -9223372036854775808. Note that the absolute value of the minimum negative number is one larger than the maximum positive number.
For signed integers, overflow occurs when two positive numbers sum to a negative, or two negative numbers sum to a positive. For unsigned integers, overflow wraps around mod 2^N. Many languages provide helper functions: Java has Math.addExact() and Math.multiplyExact() which throw ArithmeticException on overflow. C has compiler builtins like __builtin_add_overflow() in GCC/Clang.
No, Python integers have unlimited precision, so they never overflow in the traditional sense. However, when you need to simulate fixed-width two's complement behavior — for example, when porting a C algorithm that relies on wrapping — you must manually mask the result to the desired bit width using patterns like result & 0xFFFFFFFF for 32-bit behavior.
Open the Binary Code Decoder in a new tab and enter some binary patterns to see the results instantly. All conversions happen in your browser — no data is sent to any server.