🧬 Types & Qualifiers, In Depth
Conversions & casts: C changes your types behind your back
▶ Open the interactive lesson — free, no signupUnder the right (wrong) circumstances, C will cheerfully conclude that -1 is greater than 1 — and a loop meant to run zero times will run four billion times instead. Both surprises come from type conversions the language performs silently behind your back, and both have taken down real production systems. Learn the rules, and these famous bugs happen to other people.
Here's an unsettling truth: in almost every C expression you write, the values you operate on are converted to other types first — silently, by fixed rules. Most of the time the rules do what you'd hope. The rest of the time they produce some of the most famous bugs in the language. Let's learn the rules so the bugs happen to other people.
Rule 1: integer promotions — small types grow up
Types narrower than int (char, short, _Bool, bit-fields) never do arithmetic as themselves. Before any math, they're promoted to int. Yes: char + char happens as int + int.
#include <stdio.h>
int main(void) {
char a = 100, b = 100;
printf("sizeof(a+b) = %zu\n", sizeof(a + b)); /* 4, not 1! */
int sum = a + b;
printf("sum = %d\n", sum); /* 200: math happened as int,
no char overflow occurred */
unsigned char u = 255;
printf("u << 1 = %d\n", u << 1); /* promoted first... */
return 0;
}$ gcc promote.c -o promote && ./promote sizeof(a+b) = 4 sum = 200 u << 1 = 510
The promotion is why u << 1 gave 510, not 254 — the unsigned char became an int (value 255) before shifting. Promotions usually protect you from surprise overflow in intermediate results; you only feel them at the edges, like here.
🧠 Checkpoint: Two char variables are added. In what type does the addition happen?
- char
- short
- int — integer promotion widens both operands first
- Whichever char is larger
Show answer
int — integer promotion widens both operands first — Anything narrower than int is promoted to int before arithmetic. The result type is int too — assigning it back to a char is a separate (narrowing) conversion.
Rule 2: usual arithmetic conversions — finding a common type
For binary operators, both sides are converted to one common type. The dance:
▶ This spot has an interactive flow widget — open the interactive lesson to play with it.
The float side is simple: if either operand is double, everything becomes double (so 1 / 2.0 is 0.5 but 1 / 2 is 0). For integers, the higher rank wins (char < short < int < long < long long). The trap lives in the mixed-signedness case…
The signed/unsigned trap
#include <stdio.h>
int main(void) {
unsigned int u = 1;
int i = -1;
if (i > u)
printf("-1 > 1u is TRUE?!\n");
printf("i as unsigned: %u\n", (unsigned)i);
unsigned int size = 0;
printf("size - 1 = %u\n", size - 1); /* the loop-bound killer */
return 0;
}$ gcc -Wall -Wextra wat.c -o wat
wat.c:7:11: warning: comparison of integer expressions of different
signedness: 'int' and 'unsigned int' [-Wsign-compare]
$ ./wat
-1 > 1u is TRUE?!
i as unsigned: 4294967295
size - 1 = 4294967295When int meets unsigned int, the signed value converts to unsigned. -1 becomes 4,294,967,295 — so -1 > 1u is true. This bites constantly in real code because sizeof, strlen, and container sizes are all unsigned: for (int i = 0; i < size - 1; i++) with size == 0 computes 0u - 1 = 4 billion, and the loop runs off the end of the world. Compile with -Wall -Wextra: the sign-compare warning is there to save you.
🧠 Checkpoint: Why is -1 > 1u true?
- A compiler bug
- The unsigned 1 converts to signed
- The -1 converts to unsigned int, becoming 4294967295
- Comparison operators ignore sign
Show answer
The -1 converts to unsigned int, becoming 4294967295 — Same rank, mixed signedness → the signed operand converts to unsigned. Two's-complement -1 reinterprets as UINT_MAX. The comparison then honestly reports 4294967295 > 1.
Narrowing: when values don't fit
#include <stdio.h>
int main(void) {
int big = 300;
unsigned char c = big; /* wraps mod 256: well-defined */
printf("c = %d\n", c);
double d = -3.99;
int n = d; /* truncates toward zero */
printf("n = %d\n", n);
long huge = 5000000000L;
int t = huge; /* doesn't fit: impl-defined */
printf("t = %d\n", t);
return 0;
}$ gcc narrow.c -o narrow && ./narrow c = 44 n = -3 t = 705032704 # 300 mod 256 = 44; -3.99 chops to -3; 5000000000's low 32 bits = 705032704
- Integer → smaller unsigned: well-defined, wraps modulo 2ⁿ (
300 % 256 = 44). - Integer → smaller signed: result is implementation-defined if it doesn't fit (in practice: truncated two's-complement bits).
- Float → integer: the fraction is discarded — truncation toward zero, so
(int)-3.9is-3. If even the truncated value doesn't fit ((int)1e30): undefined behavior, not just a wrong number.
Explicit casts — and the void * exception
A cast is the conversion you write yourself: (type)expr. Its best use is making a conversion the compiler would do grudgingly (or not at all) loud and intentional:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int done = 7, total = 9;
printf("%.1f%%\n", 100.0 * done / total); /* no cast needed */
printf("%.1f%%\n", (double)done / total * 100); /* or be explicit */
printf("%d%%\n", done / total * 100); /* int division: 0! */
double *samples = malloc(64 * sizeof *samples); /* no cast: void* */
free(samples);
return 0;
}Don't cast malloc. void * converts to and from any object-pointer type implicitly in C — that's its whole job. int *p = malloc(n); is perfect C. The cast you often see ((int *)malloc(n)) is a C++ habit that only adds noise and can hide a missing #include <stdlib.h>.
🧠 Checkpoint: In C, int *p = malloc(10 * sizeof *p); compiles without a cast because…
- malloc returns int*
- void* converts implicitly to any object-pointer type
- The compiler infers the type from p
- It does not compile — a cast is required
Show answer
void* converts implicitly to any object-pointer type — void* is C's universal object-pointer courier; conversions to and from it are implicit by design. (C++ chose differently — there the cast IS required — which is where the habit of casting malloc leaks from.)
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Conversions gone wrong are one road into C's deepest pit — and that pit deserves its own lesson: undefined behavior.
▶ Practice this lesson interactively (with live gcc)