Signed vs Unsigned Integers: The Bit-Level Difference
The bit pattern 11111111 — eight ones. What number does it represent? If you answer 255, you are assuming unsigned. If you answer -1, you are assuming signed with two's complement. Both are correct, depending on context. The bits do not change. Only the interpretation does.
This distinction is responsible for some of the most frustrating bugs in C: sign extension surprises, unsigned wraparound in loops, and mixed signed/unsigned comparisons that defy intuition. Here is how it works at the bit level.
Unsigned Integers: Pure Binary Counting
An unsigned integer is straightforward: each bit contributes its positional value. Bit 0 adds 1, bit 1 adds 2, bit 2 adds 4, and so on. An N-bit unsigned integer ranges from 0 to 2^N - 1.
| Bit Pattern (8-bit) | Unsigned Value | Hex |
|---|---|---|
| 00000000 | 0 | 0x00 |
| 00000001 | 1 | 0x01 |
| 01111111 | 127 | 0x7F |
| 10000000 | 128 | 0x80 |
| 11111110 | 254 | 0xFE |
| 11111111 | 255 | 0xFF |
Use unsigned when your value cannot be negative: array indices, bitmasks, counters, sizes, hash values, memory addresses.
Signed Integers: Two's Complement
Two's complement is the universal standard for signed integers. Every modern CPU uses it. The most significant bit (MSB) acts as the sign bit: 0 means positive (or zero), 1 means negative.
| Bit Pattern (8-bit) | Signed Value | Why |
|---|---|---|
| 00000000 | 0 | Zero is zero |
| 00000001 | 1 | Standard binary |
| 01111111 | 127 | Largest positive (2^7 - 1) |
| 10000000 | -128 | Smallest negative (-2^7) |
| 11111110 | -2 | Invert: 00000001, +1 = 2 → -2 |
| 11111111 | -1 | Invert: 00000000, +1 = 1 → -1 |
To negate a number in two's complement: invert all bits and add 1. To find the value of a negative bit pattern: same process, same result.
// Negate 5 (00000101) in 8-bit two's complement:
// Invert: 11111010
// Add 1: 11111011 ← this is -5
// Decode 11111011 (what signed value is this?):
// MSB is 1 → negative
// Invert: 00000100
// Add 1: 00000101 → value is 5 → so pattern represents -5
Sign Extension: The Silent Bug Creator
When you assign a smaller signed type to a larger signed type, the compiler sign-extends: it copies the sign bit into all the new upper bits. This preserves the numeric value but changes the bit pattern.
int8_t small = -1; // 0xFF (11111111)
int16_t large = small; // 0xFFFF (sign-extended — still -1)
uint16_t ul = small; // 0xFFFF (same bits) but interpreted as 65535!
// This is the bug:
if (ul > 1000)
printf("ul is huge: %u\n", ul); // Prints 65535 — surprising!
Unsigned Wraparound vs Signed Overflow
Unsigned overflow is defined behavior in C — it wraps around modulo 2^N. Signed overflow is undefined behavior — the compiler can assume it never happens and optimize accordingly.
// Unsigned wraparound (defined — always works)
uint8_t u = 255;
u = u + 1; // u becomes 0 (wraps around)
// Signed overflow (undefined behavior — nasal demons)
int8_t s = 127;
s = s + 1; // UNDEFINED! Compiler can do anything
This is why for (unsigned i = n; i >= 0; i--) is an infinite loop — unsigned can never be negative. The condition i >= 0 is always true.
C Type Ranges at a Glance
| C Type | Bits | Signed Range | Unsigned Range |
|---|---|---|---|
| char / unsigned char | 8 | -128..127 | 0..255 |
| short / unsigned short | 16 | -32768..32767 | 0..65535 |
| int / unsigned int | 32 | ±2.1 billion | 0..4.3 billion |
| long long | 64 | ±9.2×10^18 | 0..1.8×10^19 |
Rules of Thumb
- Use unsigned for bitmasks, flags, sizes, and anything that interacts with bitwise operators
- Use signed for arithmetic values that can go negative
- Avoid mixing signed and unsigned in comparisons — the implicit conversion rules in C are surprising
- Use
uint32_t/int32_tfrom<stdint.h>when you need exact widths - Enable compiler warnings:
-Wsign-conversion -Wsign-compare