Common C Compilation Errors: Syntax, Linker & Runtime — With Fixes
C's compiler messages are precise and informative — if you know how to read them. This guide covers every common error you'll encounter as a C programmer, organized by category: syntax errors (stopped by the compiler), linker errors (stopped at link time), compiler warnings (allowed but dangerous), and runtime errors (crash or wrong output when running). Each entry shows the exact error message, broken code, fixed code, and a clear explanation of the root cause.
How to Read a GCC Error Message
GCC error messages follow a consistent format. Once you can decode them, every error becomes actionable in seconds.
- Filename — which file contains the error
- Line number — GCC's best guess at where to look (often one line after the actual mistake)
- Column number — horizontal position on that line
- Severity —
error(stops build),warning(build continues),note(additional context) - Message — what the compiler detected
Root cause: Every C statement must end with a semicolon. The compiler doesn't detect the missing semicolon until it sees the next token and realizes the statement was never closed — which is why the error points to the line after the mistake.
printf("Hello") // ← missing ;
return 0;printf("Hello"); // ← semicolon added
return 0;Root cause: Every opening { must have a matching }.
This error appears at the end of the file because the compiler reaches EOF while still waiting
to close an open brace.
int main() {
printf("Hi\n");
return 0;
// ← missing closing }int main() {
printf("Hi\n");
return 0;
} // ← closing brace addedUse an editor with bracket matching (VS Code, Vim with %) to spot mismatches instantly.
Root cause: In C, every variable must be declared with its type before it can be used. Using a variable name that hasn't been declared — or that was declared in a different scope — produces this error.
int main() {
x = 5; // x not declared
return 0;
}int main() {
int x = 5; // declare type first
return 0;
}Root cause: C is statically typed. Assigning a value of one type to a
variable of an incompatible type is a compile-time error. Common triggers: assigning a string
literal to an int, or passing the wrong pointer type to a function.
int age = "twenty-five"; // string → intint age = 25; // integer literal
char *name = "Alice"; // string → char*Root cause: You included <math.h> (which provides the
declaration of sqrt) but didn't tell the linker to include the math
library (libm) that contains the implementation. The -lm
flag must come at the end of the GCC command, after the source files.
gcc prog.c -o prog
undefined reference to 'sqrt'gcc prog.c -o prog -lm
# -lm links libm (math library)-l flags must come after the source/object files
in a GCC command. gcc -lm prog.c -o prog can fail on some systems because the
linker processes flags left-to-right and may not yet know it needs libm when it
encounters the flag.
Root cause: Your project has multiple .c files but you only
compiled one of them. The linker can see the call to my_function in
main.o but it's not in any of the object files it was given.
gcc main.c -o app
undefined reference to 'my_function'gcc main.c utils.c -o app
# compile ALL .c files togetherRoot cause: You wrote a function prototype (declaration) but never wrote the function body (definition). The compiler accepted the call because the prototype exists, but the linker can't find the actual code.
void greet(); // declaration only
int main() {
greet(); // linker: where is greet()?
return 0;
}
// greet body is missing!void greet(); // declaration
int main() {
greet();
return 0;
}
void greet() { // definition added
printf("Hello!\n");
}gcc -Wall -Wextra. These flags enable warnings
that catch real bugs before they become runtime failures. Better still, use
-Werror to treat all warnings as errors so your build fails until every warning
is resolved. This is standard practice in professional C codebases.
Root cause: You called a function without the compiler having seen its
declaration first. Usually means a missing #include. In C99 and later,
implicit declarations are invalid — what used to be a warning is now effectively an error.
// #include <stdio.h> missing!
int main() {
printf("Hi\n");
return 0;
}#include <stdio.h> // declares printf
int main() {
printf("Hi\n");
return 0;
}Root cause: A variable was declared but never used. This is almost always either dead code (a variable left over from a refactor) or a logic error (you intended to use the variable somewhere but forgot).
int main() {
int count = 0; // declared but never used
printf("Done\n");
return 0;
}int main() {
// Remove unused variable, or use it:
int count = 0;
printf("Count: %d\n", count);
return 0;
}Root cause: A function declared to return a value (e.g. int)
has an execution path that reaches the end without a return statement. The
returned value is undefined — the function returns garbage.
int add(int a, int b) {
int result = a + b;
// forgot: return result;
}int add(int a, int b) {
int result = a + b;
return result; // return value added
}Root cause: The format specifier in printf doesn't match the
type of the argument. %d expects int, %f expects
float/double, %s expects char*.
A mismatch produces garbage output — or a crash on some platforms.
double pi = 3.14;
printf("%d\n", pi); // %d ≠ doubledouble pi = 3.14;
printf("%f\n", pi); // %f for doubleRoot cause: The program tried to read or write a memory address it doesn't have permission to access. The most common causes are: dereferencing a NULL pointer, accessing an array out of bounds, or using memory after it has been freed.
int *p = NULL;
*p = 42; // segfault: writing to address 0int value = 0;
int *p = &value; // point to valid memory
*p = 42;gcc -g -fsanitize=address prog.c -o prog && ./prog
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x... )
#0 0x... in main prog.c:3
# AddressSanitizer tells you the exact line
Root cause: A recursive function calls itself without ever reaching a base case. Each call adds a stack frame; eventually the stack runs out of space and the OS kills the process.
int factorial(int n) {
// missing base case!
return n * factorial(n - 1);
}int factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1);
}Root cause: Memory allocated with malloc() was never released
with free(). The program doesn't crash — it silently consumes memory that is never
returned to the OS. In long-running programs (servers, daemons), this eventually exhausts
available memory.
int *arr = malloc(10 * sizeof(int));
arr[0] = 42;
// free(arr) missing — memory leakint *arr = malloc(10 * sizeof(int));
arr[0] = 42;
free(arr); // always free what you mallocTools: -Wall, AddressSanitizer, and valgrind
The right tools catch different categories of bugs. Here is when to use each one:
# Step 1: Enable all warnings during compilation
gcc -Wall -Wextra -Werror -g prog.c -o prog
# Step 2: Catch memory errors at runtime with AddressSanitizer (fast)
gcc -Wall -g -fsanitize=address -fsanitize=undefined prog.c -o prog_asan
./prog_asan
# Step 3: Deep memory analysis with valgrind (slower, more thorough)
gcc -Wall -g prog.c -o prog
valgrind --leak-check=full ./prog
-fsanitize=address)
is built into GCC/Clang, runs ~2x slower than normal, and catches buffer overflows,
use-after-free, and stack overflows with precise line numbers. Valgrind is a separate tool,
runs ~10–20x slower, but catches a broader set of memory issues including reads of
uninitialized memory. Use ASan during daily development; valgrind for thorough pre-release checks.
Quick-Reference: Error → Cause → Fix
| Error / Warning message | Stage | Root cause | Fix |
|---|---|---|---|
| expected ';' before 'X' | Compiler | Missing semicolon on previous line | Add ; to end of the line before the reported one |
| expected '}' at end of input | Compiler | Unmatched opening brace { | Add missing closing } |
| 'X' undeclared | Compiler | Variable used before declaration | Declare with type: int x; |
| implicit declaration of function 'X' | Compiler | Missing #include | Add required header, e.g. #include <stdio.h> |
| format '%d' expects 'int', has 'double' | Compiler | printf format specifier mismatch | Use correct specifier: %f for double, %d for int |
| unused variable 'X' | Compiler | Declared but never read | Remove variable or use it |
| control reaches end of non-void function | Compiler | Missing return statement | Add return value; before closing brace |
| undefined reference to 'sqrt' | Linker | Math library not linked | Add -lm at end of gcc command |
| undefined reference to 'my_func' | Linker | Source file not compiled, or body missing | Compile all .c files; write the function body |
| Segmentation fault | Runtime | NULL pointer, out-of-bounds array, use-after-free | Compile with -g -fsanitize=address to get exact line |
| Memory leak (valgrind) | Runtime | malloc without matching free | Call free(ptr) for every malloc() |
| Stack overflow / segfault in recursion | Runtime | Infinite recursion — no base case | Add a base case that stops the recursion |
Frequently Asked Questions
-Werror during development.
-lm at the end of your GCC command:
gcc prog.c -o prog -lm. The -lm flag links the math library
(libm) which contains the implementation of sqrt(),
pow(), sin(), and other math functions. Including
<math.h> only provides declarations — -lm provides
the actual compiled code.
gcc -g -fsanitize=address prog.c -o prog and run again.
AddressSanitizer will print the exact line and type of violation (NULL dereference,
buffer overflow, use-after-free). Common root causes: dereferencing a pointer that is
NULL or uninitialized, accessing an array beyond its bounds, or using memory after
calling free() on it.
-Wall enables a set of "all common" warnings — unused variables, implicit
function declarations, missing return values, always-true comparisons, and more. It does
not actually enable all possible warnings (that would be -Weverything
in Clang); it enables the set judged most useful without being noisy. Always pair it with
-Wextra for additional useful warnings, and -Werror to make
all warnings hard errors.
.c files. A linker
error means the C code compiled successfully (valid syntax and types) but the linker cannot
find the implementation of something your code calls — typically because a library flag is
missing or a source file wasn't compiled. "Undefined reference" messages are always linker
errors.