The bugs that bite every C programmer once. Each is a complete, runnable program — read it, guess what it does, then run it on real gcc in your browser.
▶ Open the interactive gallery — run every trap live, free, no signupQuick: what does 7 / 2 print?
#include <stdio.h>
int main(void) {
printf("7 / 2 = %d\n", 7 / 2);
int total = 90 + 91;
printf("average = %d\n", total / 2);
printf("100 C in F = %d\n", 100 * (9 / 5) + 32);
return 0;
}When both operands of / are integers, C performs integer division and truncates toward zero (C17 6.5.5) — the fractional part simply vanishes. That is why the average of 90 and 91 is "90", and why the Fahrenheit formula collapses: 9 / 5 is 1, so the whole conversion becomes 100 + 32. The fix costs one character: make either operand floating point — 7 / 2.0, or total / 2.0 stored in a double.
📖 Learn it properly: Arithmetic: +, −, ×, ÷ and the truncation trap →
Everyone knows % wraps around... so what is -7 % 3?
#include <stdio.h>
int main(void) {
printf(" 7 %% 3 = %d\n", 7 % 3);
printf("-7 %% 3 = %d\n", -7 % 3);
int i = -1;
printf("wrapped index: %d\n", i % 5);
return 0;
}Since C99, integer division truncates toward zero, and % must satisfy (a/b)*b + a%b == a (C17 6.5.5) — so the result of % takes the sign of the dividend. Python floors instead, which is why the two languages disagree about every negative operand. If you want a wrap-around array index that is always non-negative, use the idiom ((i % n) + n) % n.
📖 Learn it properly: Arithmetic: +, −, ×, ÷ and the truncation trap →
One missing = turns a login check into a skeleton key.
#include <stdio.h>
int main(void) {
int logged_in = 0;
if (logged_in = 1)
printf("Welcome back, admin!\n");
else
printf("Access denied.\n");
printf("logged_in is now %d\n", logged_in);
return 0;
}= assigns, == compares — and both are legal inside a condition. An assignment is an expression whose value is the value assigned, so if (logged_in = 1) stores 1 and then tests 1: nonzero, true, every single time. No error, no warning by default — just silently broken logic. Compile with -Wall and GCC will ask if you really meant it (and suggest extra parentheses when you do).
Why does this program insist it is boiling at 10 degrees?
#include <stdio.h>
int main(void) {
int temperature = 10;
if (temperature > 30);
printf("It's boiling outside!\n");
printf("(temperature is %d)\n", temperature);
return 0;
}That innocent semicolon in if (temperature > 30); is a complete, empty statement — and it is the entire body of the if. The condition is dutifully evaluated, controls nothing, and the indented printf below is just the next statement, running unconditionally. The same trap works on while and for, where while (cond); becomes an accidental infinite loop. GCC's -Wall flags the suspicious empty body.
📖 Learn it properly: if & else: teaching programs to choose →
The indentation says one thing. The compiler sees another.
#include <stdio.h>
int main(void) {
int a = 0, b = 1;
if (a == 1)
if (b == 1)
printf("a and b are both 1\n");
else
printf("a is not 1\n");
printf("done\n");
return 0;
}C attaches an else to the nearest unmatched if (C17 6.8.4.1) — indentation is invisible to the parser. Here the else pairs with if (b == 1), and since the outer condition a == 1 is false, the whole inner if/else never runs at all. Braces around every if body make the pairing explicit and the trap impossible — which is exactly why style guides insist on them.
📖 Learn it properly: if & else: teaching programs to choose →
Second place should win one medal. It wins three.
#include <stdio.h>
int main(void) {
int place = 2;
switch (place) {
case 1: printf("gold medal\n");
case 2: printf("silver medal\n");
case 3: printf("bronze medal\n");
default: printf("thanks for playing\n");
}
return 0;
}A case label is just an entry point, not a fence: once execution jumps in, it keeps flowing straight through every following case until it hits a break or the end of the switch (C17 6.8.4.2). Forgetting break is one of the oldest bugs in C — famous enough that C23 added the [[fallthrough]] attribute so you can mark the rare times you fall through on purpose. GCC's -Wimplicit-fallthrough (part of -Wextra) catches the accidental kind.
📖 Learn it properly: switch, case, default: the multi-way jump →
a = (1, 2, 3) and b = 1, 2, 3 — surely the same thing?
#include <stdio.h>
int main(void) {
int a = (1, 2, 3); /* comma: run all, keep the LAST */
int b;
b = 1, 2, 3; /* = binds tighter than , */
printf("a = %d\n", a);
printf("b = %d\n", b);
return 0;
}The comma operator evaluates left to right and yields only its last operand (C17 6.5.17), so (1, 2, 3) is 3. But comma has the lowest precedence in C — lower than = — so b = 1, 2, 3 parses as (b = 1), 2, 3: b gets 1 and the rest is evaluated and discarded. The comma earns its keep in for headers like i++, j--; anywhere else it is usually a bug wearing a disguise.
📖 Learn it properly: for loops: init, test, step — all in one line →
flags is binary 110. So why does C swear bit 2 is clear?
#include <stdio.h>
int main(void) {
int flags = 6; /* binary 110: bit 2 is set */
if (flags & 4 == 4)
printf("bit 2 is set\n");
else
printf("bit 2 is NOT set\n");
printf("flags & 4 == 4 evaluates to %d\n", flags & 4 == 4);
return 0;
}The comparison operators outrank the bitwise operators &, ^ and | in C's precedence table, so flags & 4 == 4 parses as flags & (4 == 4) — that's 6 & 1, which is 0. This ranking is a historical accident from before C had &&, and Dennis Ritchie himself listed it among his regrets. The rule to tattoo somewhere visible: always parenthesize bit tests — (flags & 4) == 4 or simply flags & 4 as the whole condition.
📖 Learn it properly: Bitwise operators: surgery on individual bits →
Shift left by two, then add one. C hears something else.
#include <stdio.h>
int main(void) {
int x = 1;
printf("x << 2 + 1 = %d\n", x << 2 + 1);
printf("(x << 2) + 1 = %d\n", (x << 2) + 1);
return 0;
}Addition binds tighter than the shift operators, so x << 2 + 1 means x << 3. This bites hardest in bit-packing code like base << 4 + offset, which quietly shifts by the wrong amount instead of adding after the shift. Same medicine as every precedence trap: parentheses around anything mixing shifts with arithmetic — they cost nothing and remove all doubt.
📖 Learn it properly: Bitwise operators: surgery on individual bits →
The most famous decimal lie in computing.
#include <stdio.h>
int main(void) {
double sum = 0.1 + 0.2;
if (sum == 0.3)
printf("equal\n");
else
printf("NOT equal: sum is %.17f\n", sum);
return 0;
}Doubles are binary fractions, and 0.1, 0.2, 0.3 all fall between the representable ones — each literal is really the nearest binary neighbor, and the rounding errors of 0.1 + 0.2 don't land exactly on the neighbor chosen for 0.3. Nothing overflowed and nothing is broken; the values differ in the 17th decimal place, and == is merciless about it. Compare floating-point with a tolerance — fabs(a - b) < 1e-9 — or work in integer units (cents, not dollars).
📖 Learn it properly: Floating point: scientific notation in bits →
sum starts at 0 automatically... doesn't it?
#include <stdio.h>
void scribble(void) {
volatile int junk[6] = {111111, 222222, 333333, 444444, 555555, 666666};
(void)junk[0];
}
int sum_to(int n) {
int sum; /* forgot = 0 */
for (int i = 1; i <= n; i++) sum += i;
return sum;
}
int main(void) {
scribble();
printf("1+2+3+4+5 = %d\n", sum_to(5));
return 0;
}Automatic (local) variables are not zeroed — sum begins life as whatever bytes the last function call left in that stack slot, which is exactly why scribble() runs first here: to salt the stack and make the garbage visible. Using an indeterminate value like this is undefined behavior (C17 6.3.2.1), and the result genuinely changes between machines, runs, and optimization levels. Initialize at the point of declaration, always — and note that static and global variables are zero-initialized, which is why this bug loves to appear only after a refactor moves a variable.
Print a double with %d — and watch your 3.14 come out of a different printf.
#include <stdio.h>
int main(void) {
double price = 3.14;
printf("with %%d: %d\n", price);
printf("with %%f: %f\n", 42);
return 0;
}printf cannot see your types — it trusts the format string and reads whatever the specifier implies from the varargs machinery. On x86-64, doubles travel in floating-point registers and ints in integer registers, so %d read an integer register nobody had set (garbage), and the later %f read the float register still holding 3.14 from the first call. A mismatched conversion specifier is undefined behavior per C17 7.21.6.1p9 — the spooky register reuse is just one way it can look. -Wall makes GCC check format strings against arguments; treat those warnings as errors.
One missing character stands between you and a segfault.
#include <stdio.h>
int main(void) {
int age = 0;
scanf("%d", age); /* forgot the & */
printf("You are %d\n", age);
return 0;
}scanf must modify your variable, so it needs the variable's address: scanf("%d", &age). Passing age instead hands scanf the value 0, which it dutifully uses as the address to store into — undefined behavior, and on our verified run an immediate segmentation fault. It won't always be so merciful: had age held stack garbage that happened to be a writable address, scanf would corrupt memory silently. GCC's format checking (-Wall) catches this one at compile time — let it.
📖 Learn it properly: stdio.h: printf, scanf & files, done right →
A perfectly normal byte in your file that looks exactly like EOF.
#include <stdio.h>
int main(void) {
char c = 0xFF; /* a perfectly normal data byte, say from an image */
if (c == EOF)
printf("Looks like the end of the file!\n");
printf("c = %d, EOF = %d\n", c, EOF);
return 0;
}getchar() returns int for a reason: it must express 257 distinct results — all 256 byte values plus EOF (-1). Squeeze the result into a char and the distinction dies: on x86-64 Linux, where plain char is signed, the honest data byte 0xFF becomes -1 and compares equal to EOF, so reading a binary file stops early. On platforms where char is unsigned (ARM Linux!), it's the opposite disaster — c == EOF is never true and the read loop never ends. Whether char is signed is implementation-defined (C17 6.2.5); the cure is always int c = getchar();.
📖 Learn it properly: stdio.h: printf, scanf & files, done right →
sizeof 'a' — one byte, obviously?
#include <stdio.h>
int main(void) {
char c = 'a';
printf("sizeof 'a' = %zu\n", sizeof 'a');
printf("sizeof c = %zu\n", sizeof c);
printf("sizeof(char) = %zu\n", sizeof(char));
return 0;
}In C, a character constant like 'a' has type int (C17 6.4.4.4p10) — it only becomes a one-byte value when you store it into a char. So sizeof 'a' is sizeof(int), 4 on this platform. C++ chose differently ('a' is a char there, size 1), making this the classic "is this file really C?" interview question. Mostly harmless trivia — until you memcpy sizeof 'x' bytes and stomp three neighbors.
Flip the bits of 0xFF and compare. C says they're different.
#include <stdio.h>
int main(void) {
unsigned char a = 0xFF;
unsigned char b = ~a; /* surely b == ~a now... */
printf("b = %u\n", b);
printf("~a = %d\n", ~a);
printf("b == ~a : %d\n", b == ~a);
return 0;
}Before almost any arithmetic happens, C silently widens anything smaller than int up to int — the integer promotions (C17 6.3.1.1). So ~a is not 8-bit bit-flipping: a becomes the int 255, and ~255 is -256. Assigning that to b truncates back to 0, but the comparison b == ~a happens in int-land: 0 versus -256. When you need byte-sized bit math to stay byte-sized, mask it: (unsigned char)~a or ~a & 0xFF.
📖 Learn it properly: Conversions & casts: C changes your types behind your back →
3 minus 5 equals 4,294,967,294 items in stock.
#include <stdio.h>
int main(void) {
unsigned int have = 3, need = 5;
printf("have - need = %u\n", have - need);
unsigned int u = 0;
if (u - 1 < 0)
printf("u - 1 is negative\n");
else
printf("u - 1 = %u, and it can NEVER be negative\n", u - 1);
return 0;
}Unsigned arithmetic is defined as modulo 2N (C17 6.2.5p9) — dip below zero and you wrap to the top. That's not a bug in the compiler; it's the deal you signed by writing unsigned. Worse, the usual arithmetic conversions drag signed values along: compare an int to an unsigned and the int converts, so -1 > 2000000000u is true. This is why for (size_t i = n - 1; i >= 0; i--) loops forever — a size_t is always ≥ 0 — and why subtracting sizes is a minefield: prefer if (have >= need) over inspecting have - need.
📖 Learn it properly: Conversions & casts: C changes your types behind your back →
Add 1 to the biggest int. What could go wrong?
#include <stdio.h>
#include <limits.h>
int main(void) {
int n = INT_MAX;
printf("n = %d\n", n);
printf("n + 1 = %d\n", n + 1);
return 0;
}Signed integer overflow is undefined behavior (C17 6.5p5) — the standard doesn't promise wraparound, garbage, or anything else. At -O0 you'll typically observe the two's-complement wrap shown here, which lulls people into trusting it; at -O2 GCC instead assumes overflow cannot happen and deletes "impossible" code — the classic victim being the safety check if (n + 1 < n), optimized away entirely. Contrast with unsigned types, whose wraparound is fully defined. If you need to detect overflow, check before the operation (n > INT_MAX - 1) or use GCC's __builtin_add_overflow.
📖 Learn it properly: Undefined behavior: here be nasal demons →
1 << 32 — zero, obviously? This program printed 1.
#include <stdio.h>
int main(void) {
unsigned int x = 1;
int n = 32;
printf("1 << 31 = %u\n", x << 31);
printf("1 << 32 = %u\n", x << n);
return 0;
}Shifting by an amount ≥ the width of the (promoted) type is undefined behavior (C17 6.5.7p3) — and this one shows why "UB" doesn't mean "crash": the program runs happily and simply gives different answers depending on who computes the shift. The x86 shl instruction masks its count mod 32, so at runtime 1 << 32 becomes 1 << 0; when the optimizer constant-folds it instead, it can produce 0 (or anything else). Note the promotion angle too: 1 << 31 already overflows a signed int — for bit masks, work unsigned and keep counts strictly below the width.
📖 Learn it properly: Undefined behavior: here be nasal demons →
Ten elements go in. The function counts two.
#include <stdio.h>
void report(int arr[]) {
printf("inside func: %zu elements?\n", sizeof(arr) / sizeof(arr[0]));
}
int main(void) {
int a[10] = {0};
printf("in main : %zu elements\n", sizeof(a) / sizeof(a[0]));
report(a);
return 0;
}A parameter declared int arr[] — even int arr[10] — is adjusted to int *arr (C17 6.7.6.3p7): arrays are never passed by value in C, only a pointer to their first element travels. So inside the function, sizeof(arr) is sizeof(int *) = 8, and the "element count" is 8/4 = 2 regardless of what was passed. The sizeof a / sizeof a[0] trick works only where the actual array is in scope; a function must be handed the length as a separate parameter. GCC even ships a dedicated warning for this one: -Wsizeof-array-argument.
📖 Learn it properly: Arrays vs pointers: the great confusion →
We zero five scores. So why is total wiped too?
#include <stdio.h>
int main(void) {
struct { int scores[5]; int total; } s = { {10, 20, 30, 40, 50}, 150 };
for (int i = 0; i <= 5; i++) /* <= goes one step too far */
s.scores[i] = 0;
printf("total = %d\n", s.total); /* we never touched total... right? */
return 0;
}An array of 5 has valid indexes 0 through 4 — the condition i <= 5 performs six writes, and scores[5] is one past the end: undefined behavior (C17 6.5.6). Here the stray write deterministically lands on total, the next struct member in memory — a tidy demonstration of how buffer overflows corrupt whatever innocent data lives next door (in real programs: other variables, heap bookkeeping, return addresses — the raw material of security exploits). The idiomatic C loop is for (i = 0; i < N; i++): start at 0, compare with <, and the count of iterations equals N.
Five letters, five bytes. What could printf get wrong?
#include <stdio.h>
int main(void) {
struct {
char word[5];
char next[9];
} s = { {'h', 'e', 'l', 'l', 'o'}, "neighbor" };
printf("word = %s\n", s.word);
return 0;
}A C string is bytes-until-\0, so "hello" really needs six bytes. The initializer {'h','e','l','l','o'} fills all five slots of word with letters and leaves no room for the terminator — and %s, which just walks memory until it meets a zero byte, strolls past the end into next. That out-of-bounds read is undefined behavior; here the struct layout makes the damage visible and repeatable. Write char word[6] = "hello"; (or let the compiler count: char word[] = "hello";) and the terminator is included automatically.
📖 Learn it properly: Strings: char arrays with a secret handshake →
The "safe" string copy that quietly leaves strings unterminated.
#include <stdio.h>
#include <string.h>
int main(void) {
struct { char dst[4]; char after[10]; } s;
strcpy(s.after, "OOPS!");
strncpy(s.dst, "gotcha", sizeof s.dst); /* copies g,o,t,c - no '\0' */
printf("dst = %s\n", s.dst);
return 0;
}strncpy has a nasty contract (C17 7.24.2.4): if the source has ≥ n characters, it copies exactly n and does not write a terminator. "gotcha" is longer than 4, so dst got g o t c and nothing else — an unterminated buffer that %s happily reads past (undefined behavior, made visible here by the neighboring field). Despite the reassuring n in the name, strncpy was designed for fixed-width fields in 1970s Unix directory entries, not for safety. Either terminate manually — dst[sizeof dst - 1] = '\0'; — or use snprintf(dst, sizeof dst, "%s", src), which always terminates.
📖 Learn it properly: string.h: the null-terminated toolbox →
The password is correct. The program says it is wrong.
#include <stdio.h>
#include <string.h>
int main(void) {
char stored[] = "hunter2", typed[] = "hunter2";
if (strcmp(stored, typed))
printf("Password accepted!\n");
else
printf("Wrong password.\n");
return 0;
}strcmp is not an "are these equal?" predicate — it's a three-way comparison for sorting: negative if the first string orders earlier, positive if later, and 0 on equality. Used bare in a condition, that 0 is falsy, so if (strcmp(a, b)) actually means "if different" — the exact opposite of what it appears to say. Always compare the result explicitly: strcmp(a, b) == 0 for equality. (Bonus trap in the same family: a == b on two char arrays compares their addresses, never their contents.)
📖 Learn it properly: string.h: the null-terminated toolbox →
The function returned an address. The variable did not survive the trip.
#include <stdio.h>
int *birthday(void) {
int candles = 21;
return &candles; /* address of a variable about to die */
}
void party(void) {
volatile int balloons[4] = {99, 98, 97, 96};
(void)balloons[0];
}
int main(void) {
int *p = birthday();
party();
printf("candles: %d\n", *p);
return 0;
}candles lives in birthday's stack frame, which is torn down the moment the function returns — the returned pointer refers to memory whose lifetime has ended, and using it is undefined behavior (C17 6.2.4). It's treacherous because it often appears to work: the stale value survives until the next call (here, party()) reuses the frame. GCC's -Wreturn-local-addr warning fires on this pattern and the compiler deliberately returns NULL instead, converting a subtle corruption into the honest crash we verified. To return data from a function, return it by value, or hand back malloc'd memory, or write into a caller-supplied buffer.
📖 Learn it properly: Scope & lifetime: who sees what, and for how long →
You freed it. malloc recycled it. Your old pointer never got the memo.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *ticket = malloc(sizeof *ticket);
*ticket = 42;
free(ticket);
int *other = malloc(sizeof *other); /* recycles the same spot */
*other = 777;
printf("my ticket says: %d\n", *ticket);
return 0;
}After free(ticket), that pointer's value is indeterminate and dereferencing it is undefined behavior (C17 7.22.3) — but the bytes don't vanish, the allocator just puts the chunk back on its shelf. Ask for the same size again and glibc hands you the very same address, which is why *ticket "worked" and showed 777: two owners, one address, and the old pointer reads the new owner's data. This is the use-after-free — a bug class behind countless real-world exploits, and invisible in testing whenever the memory hasn't been reused yet. Discipline: free(p); p = NULL; so any later dereference crashes honestly.
If one free is good, surely two is thorough?
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char *p = malloc(16);
free(p);
free(p); /* freeing the same pointer twice */
printf("done\n");
return 0;
}Calling free on a pointer that was already freed is undefined behavior (C17 7.22.3.3) — the second call hands the allocator a chunk that's already on its free list, corrupting the bookkeeping that every future malloc depends on. Historically that corruption was silently exploitable (an attacker could steer where the next allocation lands), which is why modern glibc actively checks and aborts with the message we verified. Note that "done" never printed: the abort fired before stdout was flushed. The same discipline as use-after-free saves you here — free(p); p = NULL; — because free(NULL) is defined to do nothing.
Ask for 18 quintillion bytes, then store 42 in the answer.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t huge = (size_t)-1; /* ~18 quintillion bytes */
int *p = malloc(huge);
*p = 42; /* malloc said no. p is NULL. */
printf("stored %d\n", *p);
return 0;
}malloc reports failure by returning NULL (C17 7.22.3.4) — it cannot throw, print, or apologize, and glibc rejects any request larger than PTRDIFF_MAX outright, so this one fails instantly. Dereferencing that NULL is undefined behavior; on Linux it's a segfault, on small embedded systems it can silently corrupt address 0 and keep running. Every allocation deserves the three-line tax: if (!p) { /* report and bail */ }. The subtler sibling to watch for: malloc(n * sizeof(int)) where the multiplication overflows first, allocating a tiny block that "succeeds" — calloc(n, sizeof(int)) checks that product for you.
A macro that squares 3 perfectly and butchers a + 1.
#include <stdio.h>
#define SQUARE(x) x * x
int main(void) {
int a = 4;
printf("SQUARE(3) = %d\n", SQUARE(3));
printf("SQUARE(a + 1) = %d\n", SQUARE(a + 1));
return 0;
}Macros are text paste, not function calls: SQUARE(a + 1) expands to a + 1 * a + 1, and precedence regroups it as a + (1 * a) + 1 = 4 + 4 + 1 = 9. The macro never saw the value 5 — it saw three tokens and dropped them into a precedence minefield. The armor is parentheses on every parameter and the whole body: #define SQUARE(x) ((x) * (x)) — the inner pairs guard against the argument's operators, the outer pair against operators at the call site (100 / SQUARE(5)). And even fully armored, never pass side effects: SQUARE(i++) expands to two unsequenced i++s — undefined behavior.
📖 Learn it properly: Function-like macros: power tools with no guard →
MAX(i++, 9) — fully armored in parentheses, and still wrong.
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
int i = 10;
int best = MAX(i++, 9); /* fully parenthesized... safe now? */
printf("best = %d, i = %d\n", best, i);
return 0;
}Parentheses fix precedence, but they cannot fix double evaluation — the macro pastes its argument text everywhere the parameter appears, so MAX(i++, 9) becomes ((i++) > (9) ? (i++) : (9)): the first i++ yields 10 (i→11), the comparison succeeds, and the second i++ yields 11 (i→12). This particular expansion is well-defined (?: is a sequence point) — just quietly wrong; with SQUARE(i++) the two increments are unsequenced and it's outright UB. A real function evaluates each argument exactly once; a macro makes no such promise. Keep side effects out of macro arguments — or use a function and let the optimizer inline it.
📖 Learn it properly: Function-like macros: power tools with no guard →