Articles

Two's Complement Explained: How Computers Store Negatives

Two's complement is how computers represent signed integers. How negation works, why the range is lopsided, and where overflow bugs come from.

The Lycoris Team The Lycoris Team · · 5 min read
Mathematical equations written in white chalk on a dark blackboard

Two’s complement is the scheme virtually every modern processor uses to store signed integers in binary. A negative number is represented by inverting all the bits of its positive counterpart and adding one. The payoff is that the same adder circuit handles positive and negative numbers alike, there is exactly one representation of zero, and the highest bit doubles as a sign indicator. If you have ever wondered why an 8-bit signed integer runs from -128 to 127 rather than -127 to 127, two’s complement is the answer.

The problem it solves

Unsigned binary is straightforward: with 8 bits, 00000000 is 0 and 11111111 is 255. Representing negative numbers requires giving up some of those patterns. Early designs tried a few approaches.

  • Sign-magnitude uses the top bit as a sign flag and the rest as the absolute value. It is intuitive, but it has two zeros (+0 and -0), and adding a positive and a negative number needs special-case logic.
  • One’s complement negates a number by flipping every bit. Addition is simpler, but there are still two zeros, and arithmetic needs an “end-around carry” correction.
  • Two’s complement flips every bit and then adds one. That small extra step removes the duplicate zero and makes ordinary binary addition produce correct signed results with no special cases.

Hardware designers adopted it because it costs nothing extra: the circuit that adds unsigned numbers already adds two’s complement numbers correctly.

How to negate a number

To find the two’s complement representation of a negative number, start from its positive value, invert the bits, and add one. Here is -5 in 8 bits:

 5          = 00000101
 invert     = 11111010
 add 1      = 11111011   -> -5

The same operation turns -5 back into 5, so negation is its own inverse.

Another way to read a two’s complement number is to treat the top bit as having a negative weight. In 8 bits, the place values are -128, 64, 32, 16, 8, 4, 2, 1. So 11111011 is -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5. This view makes it easy to decode any pattern by hand.

Why addition just works

Two’s complement is really modular arithmetic. An 8-bit register counts modulo 256, and the pattern for -5 is the same as the pattern for 251, because 251 is 256 minus 5. Adding 5 and -5:

  00000101   (5)
+ 11111011   (-5)
----------
 100000000   -> carry out discarded -> 00000000 (0)

The carry falls off the end of the register and the result is zero, exactly as it should be. Subtraction is implemented as addition of the negated value, so the CPU needs only one adder for both.

The lopsided range

With n bits, two’s complement covers -2^(n-1) through 2^(n-1) - 1. For common sizes:

WidthMinimumMaximum
8-bit-128127
16-bit-32,76832,767
32-bit-2,147,483,6482,147,483,647
64-bit-9,223,372,036,854,775,8089,223,372,036,854,775,807

There is one more negative value than positive, because zero occupies a slot on the non-negative side. This asymmetry has a strange consequence: the most negative number has no positive counterpart. Negating -128 in 8 bits inverts 10000000 to 01111111 and adds one, producing 10000000 again. Taking the absolute value of the minimum integer therefore returns the minimum integer in many languages, a classic source of bugs.

Sign extension and shifts

When a smaller signed value is widened to a larger type, the hardware performs sign extension: it copies the top bit into all the new high bits. The 8-bit value 11111011 (-5) becomes the 16-bit 1111111111111011, still -5. Padding with zeros instead would turn it into 251.

Right shifts come in two flavors for the same reason. An arithmetic right shift copies the sign bit into the vacated positions, so shifting -8 right by one gives -4. A logical right shift fills with zeros and treats the value as unsigned. JavaScript exposes both directly: >> is arithmetic and >>> is logical, and all JavaScript bitwise operators first convert their operands to 32-bit two’s complement integers.

How bytes are ordered in memory is a separate question; our explainer on big-endian vs little-endian covers that. Two’s complement defines what the bits mean, endianness defines what order the bytes are stored in.

Overflow: where the bugs live

Because arithmetic wraps modulo 2^n, adding 1 to the maximum value lands on the minimum. In 8 bits, 127 + 1 produces 10000000, which is -128. A CPU flags this with an overflow bit, but most high-level code never checks it.

Languages treat signed overflow differently. In C and C++, signed integer overflow is undefined behavior, so optimizing compilers may assume it never happens. Some languages wrap silently, some trap in debug builds, and some promote to arbitrary-precision integers. Knowing which rule your language follows matters.

Overflow has caused real classes of bugs:

  • Midpoint calculations. Computing (low + high) / 2 can overflow when both are large, a well-known pitfall in binary search. Writing low + (high - low) / 2 avoids it.
  • Size and length checks. If a length calculation wraps to a small or negative number, a bounds check can pass when it should fail, which is one route to a buffer overflow.
  • Signed and unsigned mixing. Comparing a negative signed value to an unsigned one can silently convert -1 into the largest unsigned value.

Floating-point numbers use an entirely different encoding with a separate sign bit, so they have positive and negative zero and no wraparound. Our guide to floating-point arithmetic covers that format.

The takeaway

Two’s complement represents a negative number by inverting the bits of its positive value and adding one. It gives a single zero, lets one adder handle both signs, and treats the top bit as a negative place value, which is why an n-bit integer ranges from -2^(n-1) to 2^(n-1) - 1. The trade-offs to remember are wraparound on overflow, the minimum value having no positive twin, and the need for sign extension and arithmetic shifts when working with signed data.

Chisato Chisato · · 6 min read

SSD Wear Leveling: How NAND Flash Stays Alive

SSD wear leveling spreads writes across NAND flash so no block wears out early. How the flash translation layer, garbage collection, and TRIM work together.

#Hardware #Computer Science #Performance
The Lycoris Team The Lycoris Team · · 4 min read

Mutex vs Semaphore: What's the Actual Difference

A mutex lets exactly one thread hold a lock at a time; a semaphore allows a fixed number of concurrent holders. Here's what that difference means in practice.

#Computer Science #Concurrency #Programming
The Lycoris Team The Lycoris Team · · 4 min read

What Is a Semaphore? Concurrency Control Explained

A semaphore is a counter that limits how many threads can access a resource at once. How they work, semaphores vs mutexes, and common pitfalls.

#Computer Science #Concurrency #Programming