Why int's negative range is larger than its positive range
- Published on
- Arnab Mondal--5 min read
Overview
- The one‑line reason
- Two’s complement in 60 seconds
- Bit‑level example (8‑bit)
- Why the extra slot is negative (not +128)
- Code pitfalls you’ll actually hit
- Takeaways
Most programming languages store signed integers using two’s complement. That choice makes the negative range one larger than the positive range. For an n‑bit signed integer, the exact range is:
- Negative to positive:
[-2^{n-1}, 2^{n-1}-1]
So with 8 bits: ([-128,127]). There are 128 non‑positive values (including 0) but only 127 strictly positive ones. Below I’ll show the intuition, a quick bit‑level walkthrough, and a few real‑world pitfalls to watch out for.
The one‑line reason
Two’s complement dedicates one more pattern to negatives because zero consumes one of the non‑negative slots. You get 2^{n-1}
negative numbers, 2^{n-1}
non‑negative patterns, but one of those is zero—leaving only 2^{n-1}−1
positive numbers.
Two’s complement in 60 seconds
- Bit width: An n‑bit signed int has 2^n total bit patterns.
- Split down the middle: Two’s complement interprets the top half (most significant bit = 1) as negative and the bottom half (MSB = 0) as non‑negative.
- Zero takes a slot: The bottom half includes 0, so there’s one fewer strictly positive number than negative numbers.
If you prefer the formula:
- Minimum value:
-2^{n-1}
- Maximum value:
2^{n-1} - 1
Notice the asymmetry: the absolute value of the minimum cannot be represented (e.g., (|-128| = 128) doesn’t fit in 8‑bit signed).
Bit‑level example (8‑bit)
Let’s enumerate just the edges to see the split:
1000_0000
→ −128 (min)- ...
0000_0001
→ +10000_0000
→ 00111_1111
→ +127 (max)
There is no +128
because that pattern (1000_0000
) is already taken by −128
.
This happens because in two’s complement, the most significant bit (MSB) is reserved to indicate the sign.
- When the MSB is
0
, the value is non-negative (0 to +127 in 8-bit). - When the MSB is
1
, the value is negative (−128 to −1 in 8-bit).
So while 1000_0000
looks like +128 in unsigned binary, in signed two’s complement it is interpreted differently:
- Invert the bits →
0111_1111
- Add 1 →
1000_0000
- Apply the negative sign → −128
Because this unique pattern is already assigned to −128, there’s no remaining binary pattern to represent +128.
That’s why the full range of an 8-bit signed integer is −128 to +127.
Why the extra slot is negative (not +128)
Short answer: in two’s complement the MSB carries a weight of -2^{n-1}
. For 8-bit values, the bit pattern 1000_0000
evaluates to -2^7 = -128
, not +128
.
Slightly longer:
- Bit weights: For two’s complement, bits
b_0..b_6
have weights2^0..2^6
, while the MSBb_7
has weight-2^7
. So the value of a pattern isb_7 * (-2^7) + Σ_{i=0..6} b_i * 2^i
. Withb_7=1
and all other bits0
, you get-128
. - Only one zero: The “non‑negative half” (MSB
0
) contains0
, which consumes one slot. That leaves2^{n-1} - 1
positive values but a full2^{n-1}
negative values. Hence the asymmetry. - Why not
-127, +128
? That distribution would require a different encoding (e.g., sign‑magnitude or a biased/excess representation). Those have drawbacks:- Two different zeros (
+0
and-0
) in sign‑magnitude. - More complicated hardware for addition/subtraction (you need sign handling and special cases).
- Losing the neat property that subtraction is “add the two’s complement,” which in hardware is just invert bits and add 1.
- Two different zeros (
- Hardware simplicity: Two’s complement keeps arithmetic fast and simple, gives a single zero, and makes overflow wrap modulo
2^n
. Assigning the MSB a negative weight is what unlocks those benefits, and it naturally makes the “extra” slot negative.
If you insisted on having +128
, you’d either need 9 bits (1 0000 0000
) or you’d switch to a different signed format and pay the cost in complexity and odd edge cases.
Why hardware loves two’s complement
- One adder to rule them all: Subtraction is just addition with negation; hardware stays simple and fast.
- Single zero: Unlike one’s complement (which had
+0
and−0
), two’s complement has exactly one zero. - Wraparound arithmetic: Overflows wrap naturally modulo 2^n, which aligns with many low‑level optimizations.
Alternatives like sign‑magnitude or one’s complement would make the ranges symmetric, but at the cost of extra complexity (two zeros, slower arithmetic, corner cases).
Code pitfalls you’ll actually hit
abs(INT_MIN)
overflow
1) In C/C++/Java‑like fixed‑width ints, the absolute value of the minimum cannot be represented. Negating it overflows.
#include <limits.h>
#include <stdio.h>
int main(void) {
int x = INT_MIN; // e.g., −2147483648 on 32‑bit int
int y = -x; // overflow: still INT_MIN in two’s complement
printf("x=%d, y=%d\n", x, y);
}
2) Off‑by‑one expectations in ranges
If you mentally assume symmetry, you might write bounds like [-max, +max]
. The real bound is [-max-1, +max]
for two’s complement (e.g., [-128, 127]
).
3) Python isn’t immune—when you fix the width
Python’s built‑in int
is unbounded, so you won’t see this asymmetry unless you pin the width (e.g., with numpy
).
import numpy as np
x = np.int8(-128)
print(x, x == -x) # -128 True (negation wraps to itself)
try:
print(abs(x)) # still -128 due to two’s complement wrap
except Exception as e:
print("error:", e)
A quick mental model
Think of the n‑bit circle of 2^n evenly spaced points. Label the bottom half (MSB=0) as non‑negative and the top half (MSB=1) as negative. Because zero occupies one spot in the non‑negative half, there’s one fewer strictly positive label left—hence the extra negative.
Takeaways
- Formula: n‑bit signed two’s complement →
[-2^{n-1}, 2^{n-1}-1]
- Asymmetry comes from two’s complement mapping and zero consuming a non‑negative slot.
- Edge case:
INT_MIN
has no positive counterpart;abs(INT_MIN)
overflows in fixed‑width types. - Reason: simpler, faster hardware and one unique zero.
Available for hire - If you're looking for a skilled full-stack developer with AI integration experience, feel free to reach out at hire@codewarnab.in