← C for People Who Already Know How to Code

C for People Who Already Know How to Code

Integer Promotion and Overflow

Every small integer becomes an int before you can blink

char and short operands are promoted to int (or unsigned int, if int can't hold all their values) before any arithmetic or comparison happens. This is invisible almost all the time, and then it isn't:

c
char c = -1;
if (c == 255) { /* ... */ } // false on platforms where char is signed:
							// c promotes to int -1, not unsigned 255

Whether plain char is signed or unsigned is itself implementation-defined (it's a distinct type from both signed char and unsigned char, whichever behavior it happens to have). Comparisons and arithmetic on raw byte data should use unsigned char explicitly if you actually mean 0–255.

Signed vs unsigned comparison is a minefield

When a signed and unsigned value of the same rank are compared or combined, the signed one converts to unsigned, not the other way around. len from strlen/sizeof is size_t, unsigned, and this is the single most common way that bites:

c
size_t len = strlen(s);
for (size_t i = len - 1; i >= 0; i--) {
	// if len == 0, len - 1 wraps to SIZE_MAX (not -1), and i >= 0 is ALWAYS true
	// for an unsigned i, this doesn't terminate the way it looks like it should
}
Try it yourself: get the compiler to warn you about this exact bug
bash
gcc -Wall -Wextra -Wsign-compare -c loop.c

Write the loop above and compile it. -Wsign-compare (included in -Wextra) should flag the i >= 0 comparison against an unsigned type as always true. Then fix it: check len == 0 first and loop with a signed index, or restructure as for (size_t i = len; i-- > 0;), which sidesteps the underflow entirely.

Signed overflow is undefined; unsigned overflow is not

Unsigned arithmetic wraps modulo 2ⁿ by definition in the standard. UINT_MAX + 1 == 0 is guaranteed, well-defined, portable behavior. Signed overflow has no such guarantee anywhere, for any operation, including the one case people assume is safe: INT_MIN / -1 and INT_MIN * -1 are both undefined too, not just addition past INT_MAX.

  • Code that wants deliberate wraparound (hashing, PRNGs, checksums) should use an unsigned type or a fixed-width type like uint32_t, never plain signed int relying on overflow to "just wrap."
  • Mixed int/size_t arithmetic in size calculations (malloc(count * sizeof(T))) is a frequent overflow source. Check for overflow before multiplying, or use a widening type, if count comes from anywhere untrusted.
  • Off-by-one loop bounds compound with this: an overflowing bound check can turn a supposedly-safe loop into one with no working upper bound at all.

The -1 into size_t trap

A function signaling "not found" by returning -1, assigned into a size_t (or any unsigned) variable, doesn't produce -1. It produces SIZE_MAX. Every check like if (index >= 0) after that assignment is dead code. The value was never going to be negative, because the type can't represent negative numbers:

c
size_t index = find_item(list, target); // returns (size_t)-1 on "not found"
if (index >= 0) {
	// always true: this is not the check you meant to write
}
Prefer a signed return type (or an explicit bool found out-parameter) for anything that needs a "not found" sentinel. Overloading an unsigned return with -1 is a bug waiting for someone to write >= 0.