Why this matters
The previous lesson gave each integer type a range. This lesson shows how negative numbers fit into those bytes, and what happens at the edges of the range. These edges cause real disasters: the Ariane 5 rocket was lost to an overflowing conversion, the "Gangnam Style" video broke YouTube's 32-bit view counter, and countless security holes start with a size calculation that wrapped around to a small number. In C, the rules for what happens are different for signed and unsigned types, and one of them is "anything at all". You need to know which.
Unsigned integers: plain binary
An unsigned integer is stored as the binary number you learned two lessons ago. An unsigned char uses 8 bits for values 0 to 255:
0000 0000 = 0
0000 0001 = 1
0111 1111 = 127
1000 0000 = 128
1111 1111 = 255
What happens past the top? The hardware simply drops the carry out of the highest bit, so the value wraps around, like a car odometer rolling from 999999 to 000000:
1111 1010 (250)
+ 0000 1010 ( 10)
-----------
1 0000 0100 (260, but only 8 bits are kept)
0000 0100 ( 4)
C defines this behavior for unsigned types: arithmetic is done modulo 2^n, where n is the number of bits. UINT_MAX + 1 is 0, and 0 - 1 is UINT_MAX. This is guaranteed and portable, and it is sometimes exactly what you want (hash functions, checksums, ring buffers). More often it is a bug, because 0 - 1 being four billion is rarely the intended meaning.
#include <stdio.h>
int main(void)
{
unsigned char odometer = 250;
odometer = odometer + 10;
printf("odometer: %d\n", odometer);
unsigned int u = 0;
u = u - 1;
printf("0u - 1 = %u (0x%x)\n", u, u);
return 0;
}
odometer: 4
0u - 1 = 4294967295 (0xffffffff)
Signed integers: two's complement
How do you store -5 in bits? Several schemes have been tried; every modern computer uses two's complement, and C23 finally requires it (C17 technically allows two older schemes, but no compiler you will meet uses them).
The idea: in an n-bit two's complement number, the top bit is worth minus 2^(n-1) instead of plus. For 8 bits, the place values are:
place value: -128 64 32 16 8 4 2 1
0000 0101: 0 0 0 0 0 1 0 1 = 5
1111 1011: -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5
1000 0000: -128 = -128
0111 1111: 64 + 32 + 16 + 8 + 4 + 2 + 1 = 127
1111 1111: -128 + 127 = -1
Consequences worth memorizing:
- The top bit acts as a sign bit: if it is 1, the number is negative.
- -1 is all ones (
0xFFin 8 bits,0xFFFFFFFFin 32 bits). - The range is asymmetric: -128 to 127 in 8 bits, -2147483648 to 2147483647 in 32. There is one more negative number than positive, because zero takes one of the non-negative patterns. So
-INT_MINdoes not fit. - To negate, invert every bit and add 1. 5 is
0000 0101; inverted,1111 1010; plus 1,1111 1011, which is -5. The C operator~inverts every bit, so~x + 1equals-x.
Why this scheme? Because addition works with exactly the same circuit as unsigned addition. Adding -5 and 7:
1111 1011 (-5)
+ 0000 0111 ( 7)
-----------
1 0000 0010 carry dropped -> 0000 0010 = 2
The CPU does not need to know whether the bits are signed. The type in your program tells the compiler which interpretation to use when comparing, dividing, printing or converting.
Same bits, two meanings
Because signed and unsigned values share bit patterns, converting between same-size types keeps the bits and changes only the interpretation (formally: the value is reduced modulo 2^n):
unsigned int bits = -1;
printf("%u = 0x%08x\n", bits, bits);
bits = -128;
printf("0x%08x\n", bits);
4294967295 = 0xffffffff
0xffffff80
bit pattern as signed as unsigned
1111 1111 -1 255 (8 bits)
1000 0000 -128 128
0111 1111 127 127
Signed overflow is undefined behavior
Here is the crucial asymmetry. Unsigned wraparound is defined. Signed overflow is undefined behavior.
#include <stdio.h>
#include <limits.h>
int main(void)
{
int big = INT_MAX;
int next = big + 1;
printf("%d + 1 = %d\n", big, next);
return 0;
}
2147483647 + 1 = -2147483648
On x86-64 at -O0, the hardware wraps just as it does for unsigned, so you see a large negative number. But the C standard says nothing about what should happen, and the optimizer is allowed to assume it never happens. For example, gcc with -O2 compiles
int f(int x) { return x + 1 > x; }
into "return 1", with no addition or comparison at all, because for every x where the behavior is defined, x + 1 > x is true. Code that "checks" for overflow by testing whether the result went negative can be deleted by the compiler for the same reason. Undefined behavior is not "you get some wrong number"; it is "the program no longer means anything". There is a whole lesson on it later.
You can make the compiler catch signed overflow at run time with the undefined behavior sanitizer:
$ gcc -std=c17 -g -fsanitize=undefined -o prog main.c
$ ./prog
main.c:7:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
2147483647 + 1 = -2147483648
The right fix is to choose a type with enough range, or to check before the operation that it cannot overflow (for example if (a > INT_MAX - b) before computing a + b, once you know if).
Common mistakes
- Counting down with an unsigned variable and expecting it to go below zero. It wraps to a huge number instead. Sizes are unsigned (
size_t), solen - 1whenlenis 0 is a classic bug. - Mixing signed and unsigned in comparisons.
-1 < 0uis false, because -1 is converted to unsigned (4294967295) before comparing. When the operands are variables, gcc warns with-Wextra(comparison of integer expressions of different signedness: 'int' and 'unsigned int'). The next module explains the conversion rules. - Assuming signed overflow wraps. It often does at
-O0and then mysteriously does not at-O2. Do not rely on it. - Negating
INT_MIN.-INT_MINoverflows (there is no +2147483648 in an int), and so doesabs(INT_MIN). - Storing out-of-range values into small signed types.
signed char sc = 200;stores -56 (200 - 256) on gcc; the conversion result is implementation-defined, and-Walldoes not even warn.-Wconversiondoes.
Recap
- Unsigned integers are plain binary and wrap modulo 2^n on overflow; this is defined behavior.
- Signed integers use two's complement: the top bit is worth -2^(n-1), -1 is all ones, the range is asymmetric, and
-x == ~x + 1. - Signed and unsigned types share bit patterns; conversion keeps the bits, reinterpreting them.
- Signed overflow is undefined behavior; the optimizer assumes it never happens. Use
-fsanitize=undefinedto catch it, and pick types or checks that prevent it.
Going further: compile int f(int x) { return x + 1 > x; } with gcc -O2 -S and look at the assembly in the .s file, then do the same with unsigned int. The unsigned version must keep the comparison, because UINT_MAX + 1 > UINT_MAX is false.