CLA (C language) common mistakes
CLA (C language) · Understand the traps and how to avoid them.
Declaring a pointer but never initializing it before dereferencing (e.g. `int *p; *p = 5;`), or using a pointer after calling `free()` on it.
Beginners learn pointer syntax before they learn object lifetime. An uninitialized local pointer holds whatever garbage is on the stack, and a freed pointer still looks like a valid address, so the code compiles cleanly and crashes or corrupts memory at run time.
What to do instead
Always initialize at declaration: int *p = NULL; and int x; int *p = &x;. After free(p), immediately set p = NULL. Before dereferencing any pointer that came from a function or malloc, test it against NULL. Compile with -Wall -Wextra and run small programs under valgrind or -fsanitize=address so the misuse is reported at the exact line.
Confusing array names with pointers: assuming `sizeof(arr)` inside a function gives the array size, or expecting `arr` to be assignable (`arr = other;`).
The array-to-pointer decay rule is taught late and is invisible at the call site. Inside a function the parameter is a pointer, so sizeof yields pointer size (often 8), not element count, and the array name is not an lvalue so assignment fails to compile.
What to do instead
Pass the element count as a separate parameter (void f(int *a, size_t n)) or use a macro/sizeof arr / sizeof arr[0] only in the scope where the array is actually declared. Remember the rule: an array name decays to a pointer to its first element in almost every expression context except sizeof, _Alignof and &.
Off-by-one and out-of-bounds array indexing, including writing a string terminator past the end of a buffer (`char s[3]; strcpy(s, "abc");`).
C performs no bounds checking, and candidates count characters in a string literal but forget the terminating '\0'. Loops written as for (i = 0; i <= n; i++) are a habitual slip from other languages or from inclusive-range thinking.
What to do instead
Size character arrays as strlen(src) + 1. Use < not <= for the upper bound of an array of length n. Prefer snprintf over sprintf and strncpy/memcpy with an explicit size. Test with -fsanitize=address so an overflow is caught rather than silently passing.
Using `=` instead of `==` in a condition, or writing `if (x = 5)` and treating it as a comparison.
Both are legal C expressions; the assignment yields the assigned value, which is nonzero and therefore 'true', so the branch is taken and the code appears to work in one direction only. Compilers accept it silently unless warned.
What to do instead
Enable -Wall, which flags 'suggest parentheses around assignment used as truth value'. When an assignment really is intended, write if ((c = getchar()) != EOF). Get in the habit of reading conditions aloud as 'is equal to' and compiling with warnings treated as errors (-Werror).
Forgetting to `#include` the right header, or misusing `printf`/`scanf` format specifiers (e.g. `%d` for a `double`, `%s` for a `char`, missing `&` in `scanf`).
C's I/O functions are variadic, so the compiler cannot check argument types against the format string; a wrong specifier is undefined behaviour that often 'works' on one platform and prints garbage on another. Omitting & in scanf passes an uninitialized value as an address and crashes.
What to do instead
Memorize the core pairs: %d/int, %f/double (also for float in printf), %c/char, %s/char *, %p/void *, %zu/size_t. Always pass addresses to scanf (scanf("%d", &n)), except for arrays which already decay. Compile with -Wformat and check that <stdio.h>, <stdlib.h>, <string.h> are included for the functions used.
Writing a function that modifies a caller's variable but passing it by value, so the change is lost.
C is pass-by-value only; the parameter is a copy. Candidates coming from languages with reference parameters expect the callee to see the original, and the bug is silent because the code compiles and the local copy changes correctly inside the function.
What to do instead
Pass the address and dereference: void inc(int *n) { (*n)++; } called as inc(&x);. For structs, pass a pointer (const struct point *p) to avoid copying and to allow modification. Draw the memory picture once: caller's x, parameter n, and the arrow from n to x.
Mishandling strings: assuming a `char` array is automatically a string, comparing strings with `==`, or calling `strlen` on an array that was never null-terminated.
C has no string type; a string is a convention (array of char ending in '\0'). == on two char * compares addresses, not contents, and functions like strlen and printf("%s") read until they find a terminator, running off the end if it is missing.
What to do instead
Use strcmp(a, b) == 0 for equality and strncmp for bounded comparison. Always terminate buffers: char s[6] = "hello"; or s[n] = '\0'; after manual copying. Use fgets(s, sizeof s, stdin) instead of gets, and remember fgets may keep the newline.
Losing track of dynamically allocated memory: forgetting to `free`, freeing inside a loop on the same pointer, or freeing a pointer that was not returned by an allocator.
C has no garbage collector, and malloc/free are taught as syntax rather than as a matched pair with ownership rules. Early returns and error paths are the usual leak sites, and free on a stack address or on an interior pointer is undefined behaviour.
What to do instead
Adopt a rule: every malloc/calloc/realloc has exactly one matching free on the same base pointer, in the same function or documented as transferred ownership. Free on every exit path, including error branches. Never free a pointer into the middle of a block or one not from the heap. Use valgrind's leak summary as a routine check.
Writing preprocessor directives incorrectly: a missing `#endif`, a function-like macro without parentheses around parameters (`#define SQ(x) x*x`), or a missing include guard.
The preprocessor is textual substitution, not a function call, so SQ(a+1) expands to a+1*a+1. Multiple inclusion of a header causes redefinition errors that appear far from the real cause, and unbalanced conditionals produce confusing cascade errors.
What to do instead
Parenthesize every parameter and the whole body: #define SQ(x) ((x)*(x)), and prefer an inline function or static inline where possible. Guard every header with #ifndef NAME_H / #define NAME_H / #endif. Compile with -E to inspect the expanded output when a macro misbehaves.
Using a struct without knowing the difference between `.` and `->`, or forgetting the semicolon after a struct definition, and reading file data without checking the return value of `fopen`/`fscanf`.
. accesses a struct object, -> accesses through a pointer; mixing them is a type error that beginners hit constantly. The struct definition is a declaration and needs a terminating semicolon, which the compiler reports on the next line. File operations fail silently at run time if the return value is ignored.
What to do instead
Read p->field as (*p).field and pick the operator based on whether you hold the struct or a pointer to it. Always end struct name { ... }; with a semicolon. Check if (fp == NULL) { perror("open"); return 1; } and check fscanf against the expected number of conversions before using the values.