Signed vs Unsigned Binary Numbers

How the same bits mean different numbers as signed or unsigned, the value range of 8, 16, 32 and 64-bit integers, and how overflow and wraparound work.

Signed vs Unsigned Integers

The meaning of a bit pattern is assigned by the signed vs unsigned binary convention you adopt, and getting that assignment backwards is the single most common error in low-level programming. This contract is explicit: what ranges each width and interpretation actually hold, where the edge cases bite, and why a carry flag and an overflow flag are not interchangeable warnings.

One Bit Pattern, Two Meanings

Take the byte 11111111. As an unsigned integer, it is 255, the maximum value an 8-bit unsigned integer can hold. As a signed two's complement integer, it is -1. The same physical bits, two different decimal values, and no amount of staring at the bits themselves will tell you which one is intended.

This duality scales to every width. Two's complement makes this work cleanly because it has a single zero and a single representation for every value. Two's complement avoids both problems, which is why it is the universal choice for modern processors and languages, but the signed ranges quoted here are two's complement ranges, not universal laws of binary arithmetic.

Range Formulas for n Bits

The unsigned range for any n-bit width is 0 to 2^n - 1. The formula is simple because there is no sign bit consuming a position. The asymmetry between the negative and positive limits is deliberate: the negative side has one extra value because zero occupies the all-zeros pattern, and the all-ones pattern is -1, not a negative zero. So 8-bit signed 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, and 64-bit from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.

The off-by-one error in the negative direction is the classic trap. For 8-bit, that is -128 and 127, not -127 and 128. If you are checking bounds in code and you get these wrong, you will accept an out-of-range value as valid or reject a valid one, and the failure will be silent until the data has already corrupted something.

8-Bit Signed Range in Practice

The asymmetry is not a mistake. There is no other value it could be, because 127 plus one, in 8-bit signed arithmetic, wraps to -128. This asymmetry has a practical consequence: the absolute value of the minimum signed value cannot be represented in the same width. This is not a bug in your code; it is a limitation of the representation.

When you widen an 8-bit signed value to 16 bits, you must sign-extend, which means copying the MSB into the new high bits. This sign extension error is a classic source of subtle bugs, especially when mixing types in C or JavaScript.

Unsigned Integer Range and the Wrap-Around Trap

The unsigned integer range for n bits is always 0 to 2^n - 1, with no negative values and no sign bit. The absence of a sign bit means the entire bit width is available for magnitude, so the unsigned maximum is always exactly double the signed maximum plus one, for the same width. Adding 1 to 255 in 8-bit unsigned gives 0, and the carry out of the MSB is lost. This is not an error in the hardware; it is the defined wrap-around behavior.

Wrap-around is also why you should never use an unsigned type for a loop index that must terminate. The classic failure mode is a for loop written as for (i = 10; i >= 0; i--). When i reaches 0 and decrements, it becomes 4,294,967,295, the condition i >= 0 is still true, and the loop runs for another four billion iterations. The fix is to use a signed index, or to check the condition before decrementing.

Binary Overflow vs Carry: Two Flags, Two Meanings

Binary overflow and carry are the two ways an arithmetic result can exceed its container, and confusing them is a debugging disaster. An overflow flag, on the x86 family and most other processors, means the signed two's complement result is out of range, so the signed interpretation is wrong. These are not the same condition, and a single operation can set one flag without setting the other.

Consider adding 127 and 1 in 8-bit signed. The result is 128, which does not fit in the signed range, so overflow is set. But there is no carry out of the MSB, because 127 plus 1 is 128, which fits in the 8-bit unsigned range of 0 to 255. So you get overflow set and carry clear.

Now consider adding 255 and 1 in 8-bit unsigned. The result is 256, which does not fit in 8 bits. Carry set, overflow clear.

The two cases are mutually exclusive for the same operand widths: adding two positive signed numbers that exceed the signed maximum sets overflow but not carry; adding two unsigned numbers that exceed the unsigned maximum sets carry but not overflow. The only way to set both is to add two numbers where the signed result is wrong and there is also a carry out, which happens when you add two positive numbers that both have the MSB set, like 127 plus 127 in 8-bit, which gives -2 signed and 254 unsigned, and these numbers appear constantly in file formats, network protocols, and database keys. But JavaScript complicates the picture: its bitwise operators, including the shift operators, coerce their operands to 32-bit signed integers, perform the operation, and then convert the result back to a 64-bit floating-point number. This is not a quirk; it is the defined behavior.

Detecting Overflow in Practice

When you are working in assembly or close to the hardware, the overflow flag is a hardware signal you can branch on directly. In C, there is no portable overflow flag, so you have to detect overflow by examining the operands before the operation, or by using compiler builtins like __builtin_add_overflow in GCC and Clang. The portable trick for signed addition overflow is to check the signs of the operands against the sign of the result: if you add two positive numbers and get a negative result, or add two negative numbers and get a positive result, overflow occurred.

For unsigned addition, overflow is exactly the carry out of the MSB. In C, you can detect it by checking whether the result is less than either operand, because unsigned arithmetic wraps modulo 2^n, so a wrapped result is always smaller than at least one of the operands. This is the if (a + b < a) idiom, and it is reliable for unsigned types because the wrap-around is defined behavior in C.

For multiplication, the check is more expensive. The common trick for unsigned multiplication overflow is to divide the maximum value by one operand and compare the other operand to the quotient. If the other operand is larger, the product overflows. This requires a division, which is slow, which is why compilers provide builtins that emit a single multiply instruction with an overflow flag read on many architectures.

There is a family of bit tricks for overflow-adjacent problems. For example, the average of two unsigned integers without overflow is computed as (a & b) + ((a ^ b) >> 1), which works because the carry from the addition is captured by the AND and the half-sum is computed without carrying. This is faster than adding a and b and dividing, because a + b might overflow, and the bit trick avoids the overflow entirely. The absolute value of a signed integer in two's complement can be computed as (x ^ (x >> (width-1))) - (x >> (width-1)), where the arithmetic shift propagates the sign bit, but this fails for the minimum value, as noted earlier.

The 32-bit integer max value of 2,147,483,647 is also the maximum file size for the classic FAT32 filesystem, which is why files larger than 2 GB fail on older systems unless they use a different API.

Another common failure is the sign extension of masks. The correct mask for the low 32 bits of a 64-bit value is 0xFFFFFFFF, but you must ensure it is not sign-extended, which means casting it to an unsigned type first.

Implicit conversion between signed and unsigned in C is a rich source of bugs. A comparison like if (a < b) where a is -1 and b is 1 fails, because -1 converts to the huge unsigned value and is no longer less than 1. This is not a compiler bug; it is the defined behavior of the language, and it means you must be explicit about types when comparing values that can be negative.

When you need to extract a bit field from a value, the standard trick is (x >> start) & ((1 << n) - 1). Both of these assume the shift count is less than the width of the type; shifting by the width or more is undefined behavior in C, even though many processors mask the count.

The One Number That Defines the Whole Page

The 8-bit signed range of -128 to 127 is the one range that every programmer learns wrong at least once.

That asymmetry propagates to every wider width, and it is the reason the 32-bit integer max value is 2,147,483,647 and not 2,147,483,648, and the reason the minimum signed value has no positive counterpart in the same width.

Before you rely on any of these ranges in production code, remember that the exact size of an int in C is not fixed by the standard. The ranges in the table above are for the fixed-width types like int8_t and int32_t, which are guaranteed to be exactly that width, not for the bare int type. If you need a specific range, use the fixed-width types from stdint.h, and if you need to know what your platform's int actually is, check the value of UINT_MAX, which is defined in limits.h and tells you the answer at compile time.

Signed vs Unsigned Binary: Ranges and Overflow

What is the unsigned range for an 8-bit value?

The unsigned range for any n-bit width is 0 to 2^n - 1. For 8 bits, that is 0 to 255, with 255 being the maximum unsigned value.

Why is the signed 8-bit minimum -128 and not -127?

The asymmetry exists because zero occupies the all-zeros pattern, and the all-ones pattern is -1, not a negative zero. So 8-bit signed runs from -128 to 127, with the negative side having one extra value.

What happens when you add 127 and 1 in 8-bit signed arithmetic?

The result is 128, which does not fit in the signed range, so the overflow flag is set. However, there is no carry out of the MSB because 128 fits in the 8-bit unsigned range of 0 to 255, so carry remains clear.

How can you detect unsigned addition overflow in C?

In C, unsigned addition overflow is detected by checking whether the result is less than either operand.

What is the maximum file size for FAT32 and why?

The maximum file size for FAT32 is 2,147,483,647 bytes, which is the 32-bit integer max value. This is why files larger than 2 GB fail on older systems unless they use a different API.

Why should you use fixed-width types like int32_t instead of bare int?

The exact size of an int in C is not fixed by the standard, so ranges can vary by platform. Fixed-width types like int32_t from stdint.h are guaranteed to be exactly that width, ensuring predictable behavior across different systems.