Layerbit
Computer Science

Two's Complement Explained: How Computers Store Negative Numbers

Every mainstream processor stores signed integers the same way, and almost every overflow bug, unexpected -56 and broken byte parser comes from forgetting how. Two's complement is a few lines of arithmetic once you see why it works. This guide builds it from first principles, then walks through the bugs it causes in real code, from Java bytes to JavaScript bit operators to the year 2038.

The short version

  • In an n-bit two's-complement number, the top bit is worth −2n−1 instead of +2n−1. Every other bit keeps its usual positive weight.
  • To negate a number: invert every bit and add one. 5 is 00000101, so −5 is 11111011.
  • An 8-bit signed value runs from −128 to 127; 16-bit from −32,768 to 32,767; 32-bit from −2,147,483,648 to 2,147,483,647.
  • The same bits mean different numbers as signed and unsigned: 11111111 is 255 or −1. Most bugs are a value written as one and read as the other.
  • Overflow wraps around silently in most languages: 2,147,483,647 + 1 becomes −2,147,483,648 in 32 bits.

The problem: binary has no minus sign

Memory holds bits, so a negative number needs an encoding. The obvious idea, sign and magnitude, uses the top bit as a sign flag and the rest as the value: 00000101 is 5 and 10000101 is −5. It has two flaws. There are two zeros, 00000000 and 10000000, which every comparison has to treat as equal. And adding a positive and a negative number needs different circuitry from adding two positives. Ones' complement, used by some early machines, negates by inverting every bit; it still has a negative zero and needs an "end-around carry" when adding.

Two's complement removes both problems. There is exactly one zero, and the ordinary binary adder that handles unsigned numbers gives the right answer for signed numbers too, with no special cases. That is why every processor in common use, from phones to servers, uses it, and why C++20 and C23 finally made it the only representation the language standards allow.

How it works

Think of an 8-bit number as eight weighted positions. Unsigned, the weights are 128, 64, 32, 16, 8, 4, 2 and 1. Two's complement changes exactly one thing: the top bit is worth −128. So 10000000 is −128, 10000001 is −128 + 1 = −127, and 11111111 is −128 + 127 = −1.

BitsUnsignedSigned (two's complement)
0000 000000
0000 000111
0111 1111127127 (largest)
1000 0000128−128 (smallest)
1100 1000200−56
1111 1011251−5
1111 1111255−1

An equivalent way to see it: a negative number −x is stored as 2n − x. In 8 bits, −5 is stored as 256 − 5 = 251. This is arithmetic modulo 2n, the same wrap-around you get on a clock face, which is exactly why addition works without special cases: 7 + (−5) is 7 + 251 = 258, and 258 modulo 256 is 2.

Negating: invert and add one

To find the bits of −x, flip every bit of x and add one. Flipping every bit gives 2n − 1 − x, so adding one gives 2n − x, which is the definition above.

  5  =  0000 0101
 ~5  =  1111 1010     invert every bit
+1   =  1111 1011     = −5  (0xFB, or 251 read as unsigned)

check: 0000 0101 + 1111 1011 = 1 0000 0000 → the carry falls off the top → 0

The same rule works in reverse, so applying it twice returns the original number. One value has no positive counterpart: the most negative number. In 8 bits, inverting −128 (10000000) and adding one gives 10000000 again. That is why abs(INT_MIN) in C is undefined behavior and returns a negative number in practice, and why Math.abs(Integer.MIN_VALUE) in Java returns −2,147,483,648.

Ranges and sign extension

WidthSigned rangeUnsigned rangeTypical types
8 bits−128 … 1270 … 255int8_t, Java byte, uint8_t
16 bits−32,768 … 32,7670 … 65,535short, int16_t
32 bits−2,147,483,648 … 2,147,483,6470 … 4,294,967,295int, int32_t, SQL INTEGER
64 bits−9,223,372,036,854,775,808 … 9,223,372,036,854,775,8070 … 18,446,744,073,709,551,615long, int64_t, SQL BIGINT

When a signed value moves into a wider type, it is sign-extended: the top bit is copied into every new bit, so −5 in 8 bits (11111011) becomes 11111111 11111011 in 16 bits, still −5. An unsigned value is zero-extended instead. Mixing the two up is how a byte of 0xFB read from a file turns into −5 when you expected 251, or into 65,531 when you expected −5.

The bugs it causes in real code

  • Java's signed bytes. Java has no unsigned byte, so (byte) 200 is −56, and a byte array read from a network socket prints negative values for everything above 127. Mask to recover the unsigned value: b & 0xFF.
  • JavaScript bit operators work in 32 bits. Numbers are 64-bit floats, but |, &, ^, ~, << and >> convert their operands to signed 32-bit integers first. So 0xFFFFFFFF | 0 is −1, and 2147483647 + 1 | 0 is −2,147,483,648. The unsigned right shift x >>> 0 reinterprets the result as unsigned, which is why it appears in hashing code. For wider values use BigInt.
  • Arithmetic versus logical shift. Shifting a negative number right with >> copies the sign bit in from the left, so −128 >> 1 is −64. A logical shift (>>> in Java and JavaScript) fills with zeros and produces a large positive number instead.
  • Silent overflow. In C, signed overflow is undefined behavior, and compilers may optimise on the assumption that it never happens; in Java, C# and Go it wraps. Rust panics in debug builds and wraps in release builds unless you use the checked_, wrapping_ or saturating_ methods. Python's integers never overflow, which hides the problem until the value reaches a database or a C library.
  • Counters that hit the limit. YouTube's view counter was a signed 32-bit integer until 2014, when one video approached 2,147,483,647 views and the counter was moved to 64 bits. The same limit is behind the year 2038 problem: signed 32-bit Unix time runs out at 03:14:07 UTC on 19 January 2038.
  • Narrowing conversions. In 1996 the first Ariane 5 rocket was lost when software converted a 64-bit floating-point velocity into a 16-bit signed integer, the value was too large to fit, and the resulting exception shut down the guidance system.

Reading signed values from bytes

File formats and network protocols specify both a width and a signedness for every field, plus a byte order. To decode a signed 16-bit big-endian value by hand, combine the bytes as unsigned and then subtract 216 if the top bit is set:

// bytes FF 38 → 0xFF38 = 65,336 as unsigned
const u = (bytes[0] << 8) | bytes[1];
const s = u >= 0x8000 ? u - 0x10000 : u;   // −200

// or let the platform do it
new DataView(buffer).getInt16(0, false);   // false = big-endian

Most languages provide the second form: DataView in JavaScript, struct.unpack('>h', …) in Python, ByteBuffer.getShort() in Java, and binary.BigEndian.Uint16 followed by a conversion to int16 in Go. Use them rather than hand-rolled shifts whenever you can.

See any number as bits Enter a value and see its two's-complement bits at 8 to 128 bits, signed and unsigned readings, both byte orders and IEEE-754 floats. Click a bit to flip it.
Open the number base converter

Frequently asked questions

Write the positive number in binary at the chosen width, invert every bit, then add one. For −12 in 8 bits: 12 is 00001100, inverted it is 11110011, and adding one gives 11110100, which is 0xF4. Equivalently, subtract the magnitude from 2 to the power of the width: 256 − 12 = 244, which is 11110100 in binary.

Because it has a single representation of zero and lets the same adder circuit handle signed and unsigned numbers. Addition, subtraction and multiplication of the low bits work identically for both, so processors need no separate signed arithmetic for those operations. Sign-and-magnitude and ones' complement both have a negative zero and need extra logic to add numbers of different signs.

Eight bits have 256 patterns. Zero takes one of the patterns with the top bit clear, leaving 127 positive values, and all 128 patterns with the top bit set are negative, from −128 to −1. The extra negative value is the price of having a single zero; it is also why negating the most negative number overflows back to itself.

They are two interpretations of the same bits. Unsigned treats every bit as a positive power of two, so 8 bits hold 0 to 255. Signed two's complement gives the top bit a negative weight, so 8 bits hold −128 to 127. The bit pattern 11001000 is 200 unsigned and −56 signed. Choose unsigned for sizes, counts and raw bytes, and signed for quantities that can go below zero.

Hexadecimal of a negative number depends on the width. Take the two's-complement bits at that width and group them in fours: −5 is FB in 8 bits, FFFB in 16 bits and FFFFFFFB in 32 bits. In JavaScript, (−5 >>> 0).toString(16) gives the 32-bit form, fffffffb. The number base converter shows every width at once.