Open any C book and it says integers use “two’s complement” representation. Most programmers accept this and move on. But understanding two’s complement explains why -1 == 0xFFFFFFFF, why negating INT_MIN overflows, and why the cast from unsigned to signed can produce negative numbers. It is worth understanding properly.
The Problem Two’s Complement Solves
You need to represent negative numbers using only bits. The naive approach — “sign and magnitude” where the top bit is the sign — has two problems: addition requires special handling for the sign bit, and you get two representations of zero (+0 and -0). Computers need addition to work with a single circuit for both positive and negative numbers.
How Two’s Complement Works
/* For a signed 8-bit integer (int8_t): */
Bit pattern: 0111 1111 = +127 (0x7F)
Bit pattern: 0000 0001 = +1
Bit pattern: 0000 0000 = 0
Bit pattern: 1111 1111 = -1 (0xFF)
Bit pattern: 1111 1110 = -2 (0xFE)
Bit pattern: 1000 0001 = -127 (0x81)
Bit pattern: 1000 0000 = -128 (0x80 — most negative)The rule: to negate a number, invert all bits and add 1.
/* Negate +5 (0000 0101) to get -5: */
Step 1: Invert all bits: 1111 1010
Step 2: Add 1: 1111 1011 = 0xFB = -5 ✓
/* Verify: 5 + (-5) should be 0 */
0000 0101 (+5)
+ 1111 1011 (-5)
-----------
1 0000 0000 = 0 (the carry out of the top bit is discarded) ✓This is why -1 is all ones: negate +1 (0000…0001): invert → (1111…1110), add 1 → (1111…1111). Every bit is set.
Verifying in C
#include <stdio.h>
#include <stdint.h>
int main() {
int8_t a = -1;
printf("-1 as hex: 0x%02Xn", (uint8_t)a); /* 0xFF */
int8_t b = -5;
printf("-5 as hex: 0x%02Xn", (uint8_t)b); /* 0xFB */
int32_t c = -1;
printf("-1 as 32-bit hex: 0x%08Xn", (uint32_t)c); /* 0xFFFFFFFF */
/* Two's complement negation */
int8_t x = 5;
int8_t neg_x = ~x + 1; /* invert + add 1 */
printf("~5 + 1 = %dn", neg_x); /* -5 */
return 0;
}The INT_MIN Problem
#include <stdio.h>
#include <limits.h>
int main() {
printf("INT_MIN = %dn", INT_MIN); /* -2147483648 */
printf("-INT_MIN = %dn", -INT_MIN); /* undefined behavior! */
/* Why? Negate INT_MIN (1000...0000):
Invert: 0111...1111 = INT_MAX
Add 1: 1000...0000 = INT_MIN again
The result doesn't fit in a signed int — overflow! */
return 0;
}The range of two’s complement for N bits is -2^(N-1) to 2^(N-1) - 1. For int32_t: -2147483648 to +2147483647. Note the asymmetry: one more negative number than positive. This is why negating INT_MIN overflows — there is no positive representation for 2147483648 in 32 bits.
Arithmetic Right Shift
#include <stdio.h>
#include <stdint.h>
int main() {
int32_t neg = -8;
printf("-8 >> 1 = %dn", neg >> 1); /* -4 (arithmetic shift) */
/* bit pattern: 1111...11111000 >> 1 = 1111...11111100 = -4 */
uint32_t uns = (uint32_t)-8; /* 0xFFFFFFF8 */
printf("0xFFFFFFF8 >> 1 = %un", uns >> 1); /* 0x7FFFFFFC = 2147483644 */
return 0;
}For signed integers, right shift “sign-extends” — the top bit is replicated into the vacated positions. This is “arithmetic right shift” and is the natural division-by-2 for negative numbers. For unsigned integers, right shift fills with zeros (logical shift). This is why bitwise operations should always use unsigned types when you want predictable shifting behavior.
Two’s Complement and Casting
#include <stdio.h>
#include <stdint.h>
int main() {
uint8_t u = 200;
int8_t s = (int8_t)u; /* 200 = 1100 1000 — as signed: -56 */
printf("(int8_t)200 = %dn", s); /* -56 */
int8_t neg = -56;
uint8_t pos = (uint8_t)neg; /* -56 = 1100 1000 — as unsigned: 200 */
printf("(uint8_t)-56 = %un", pos); /* 200 */
return 0;
}Casting between signed and unsigned types just reinterprets the bit pattern. No bits change — only the interpretation changes. This is defined behavior when converting from unsigned to signed if the value fits; implementation-defined if it doesn’t (but two’s complement machines do the obvious thing).
You can verify all of these bit patterns in our c code compiler — print values as hex with %X and signed with %d to see both interpretations at once.
TL;DR
- Two’s complement: negate by inverting all bits and adding 1
-1is all ones:0xFF,0xFFFF,0xFFFFFFFFdepending on width- The range is asymmetric: one more negative number than positive
- Negating
INT_MINis undefined behavior — the result doesn’t fit - Right shift on signed values is arithmetic (sign-extends); on unsigned, logical (fills zeros)
- Casting between signed and unsigned reinterprets bits — no computation occurs
- Since C23, two’s complement is required by the standard; previously implementation-defined but universally used