C CLP common mistakes
C CLP · Understand the traps and how to avoid them.
Treating an array parameter such as void f(int a[10]) as carrying its size, and calling sizeof(a) inside f expecting 40 on a 32-bit int platform.
The parameter is adjusted to int *, so sizeof yields the pointer size (4 or 8). Candidates memorize 'arrays decay to pointers' as a slogan but never connect it to sizeof, so they misjudge buffer capacities and loop bounds in helper functions.
What to do instead
Always pass the element count as a separate size_t parameter, or pass a pointer-to-array (int (*a)[10]) when the extent is fixed. When reviewing code, scan every function body for sizeof applied to a parameter declared with [] and rewrite it. Verify with a quick printf of sizeof in a scratch program.
Believing that malloc zero-fills memory, or that free() sets the caller's pointer to NULL, and then dereferencing or double-freeing after release.
Heap memory is uninitialized and free() takes the pointer by value, so the caller's copy is unchanged. Candidates confuse calloc with malloc and assume free performs cleanup of the variable itself, which produces use-after-free and double-free defects in the questions that trace pointer state.
What to do instead
Use calloc when zeroed storage is required, otherwise memset explicitly. After free(p), immediately assign p = NULL and treat any later dereference as a bug. When tracing a question, annotate each pointer's state (live, freed, dangling) line by line rather than tracking only the variable's value.
Predicting the value of an expression like i = i++ + ++i, or f(i++, i++), by assuming left-to-right evaluation and a single side effect per object.
C leaves the order of evaluation of most operands unspecified, and modifying an object twice between sequence points (or modifying and reading it without sequencing) is undefined behavior. Candidates apply intuition from languages with defined evaluation order and pick a concrete number that the standard does not guarantee.
What to do instead
Classify each subexpression: is it unspecified (order of function arguments, order of + operands) or undefined (multiple unsequenced modifications)? For undefined cases the correct answer is 'undefined behavior', not a computed value. Memorize the sequence points: &&, ||, ?:, comma, full expressions, and function return.
Assuming a char is signed (or unsigned) and mispredicting comparisons, array indices from getchar(), and the result of (char)0xFF.
Plain char has implementation-defined signedness; on common x86 builds it is signed, so 0xFF becomes -1 and compares less than 0. Candidates also forget that getchar() returns int precisely so EOF is distinguishable, and store the result in char, losing the EOF test.
What to do instead
Store getchar()/fgetc() results in int, never char. Use unsigned char for byte manipulation and when indexing tables by character value. When a question hinges on char signedness, state the assumption explicitly and note that the standard does not fix it; use <limits.h> CHAR_MIN to decide.
Forgetting that the preprocessor is textual and that macros lack type checking or scoping, so MAX(a++, b) increments twice and #define SQUARE(x) x*x mis-evaluates SQUARE(1+2).
Candidates read macros as if they were functions with argument passing. Because substitution is token-based and unparenthesized, side effects duplicate and precedence changes silently. Object-like macros also ignore block scope, which breaks questions about shadowing.
What to do instead
Parenthesize every parameter and the whole body: #define SQUARE(x) ((x)*(x)). Prefer static inline functions when a real function is acceptable. Before answering, expand the macro by hand with the actual argument text and check for duplicated side effects and operator precedence collisions.
Misreading declarations such as int *p[10], int (*p)[10], and int (*f)(void), or assuming a const qualifier on a pointer also makes the pointee const.
C declaration syntax is read outward from the identifier, and [] and () bind tighter than *. Candidates skim left to right and collapse the distinction between array of pointers and pointer to array, or between pointer-to-const and const-pointer, which changes both assignment legality and sizeof.
What to do instead
Apply the right-left rule: start at the name, move right for [] and (), then left for *, resolving parentheses innermost first. For qualifiers, read const as applying to whatever is immediately to its left (or to the type if nothing is). Practice by writing the English reading of each declaration before choosing an answer.
Writing while (!feof(fp)) { fgets(buf, n, fp); ... } and processing a stale buffer one extra time at end of file.
feof only becomes true after a read has already failed, so the loop body runs once more on the previous contents. Candidates treat feof as a look-ahead predicate, which is the single most common file-I/O defect in exam traces.
What to do instead
Test the read itself: while (fgets(buf, sizeof buf, fp) != NULL) { ... }. After the loop, distinguish EOF from error with feof and ferror. For fread, compare the returned item count to the requested count. Also check fclose's return value on write streams.
Assuming struct layout is packed, or that two structurally identical struct types are compatible, and miscomputing sizeof or assignment legality because of padding and tags.
Each member is aligned to its own alignment requirement, so padding appears between and after members; and two structs declared in separate declarations are distinct types even with identical members unless one is a typedef of the other. Candidates add member sizes only and ignore tags, producing wrong sizeof values and wrong compatibility judgments.
What to do instead
Compute sizeof by laying out members with explicit offsets: round each offset up to the member's alignment, then round the total up to the strictest member alignment. For compatibility, compare the actual type identity (same tag in the same scope, or the same typedef), not the member list. Note that offsetof gives the answer without guessing.
Confusing the three meanings of static and misjudging linkage: expecting a static local to be reinitialized on each call, or expecting a static file-scope function to be callable from another translation unit.
static at block scope means static storage duration; at file scope it means internal linkage. Candidates learn one meaning and apply it everywhere, so they predict wrong values for counters across calls and wrong linker behavior for helper functions.
What to do instead
Build a two-axis table: storage duration (automatic, static, allocated) versus linkage (none, internal, external). Place each declaration in it before answering. Remember a static local is initialized once, before main runs, and a file-scope static is invisible to the linker outside its translation unit.
Relying on implicit conversions in mixed signed/unsigned arithmetic, so that -1 < 1u evaluates false and a loop counting down with an unsigned index never terminates.
The usual arithmetic conversions promote the signed operand to unsigned when ranks are equal, wrapping negative values to large positives. Candidates evaluate the expression as if both operands kept their signed meaning, and they also miss integer promotion of char and short to int before arithmetic.
What to do instead
Before evaluating, write the common type of each binary operation and convert both operands to it explicitly. Compile with -Wall -Wextra -Wsign-compare (or /W4) and treat sign-compare warnings as errors in practice. For countdown loops, use a signed index or test before decrementing rather than after.