Compiler Warnings & Flags
The compiler is the first code reviewer you have, and it works for free. With the right options, GCC and Clang point out type mismatches, lost precision, wrong printf formats and many real bugs before the program ever runs. With the wrong options, or none, they stay silent. Every C team has a standard set of flags (command-line options), and new joiners are expected to understand them.
In this lesson you will see which warnings -Wall and -Wextra really turn on, add the extra warnings that catch more bugs, decide when to use -Werror, pick optimisation levels for debug and release builds, pass settings with -D and -I, and meet the hardening flags used for production software on Linux.
One program, many warnings
This program compiles with no options at all. Look at what a stricter set of flags finds:
#include <stdio.h>
#include <string.h>
double average(const int marks[], int n)
{
int total = 0;
for (int i = 0; i < n; i++) {
total += marks[i];
}
return (double) total / n;
}
int main(int argc, char *argv[])
{
int marks[] = {72, 85, 64, 90};
int rounded = average(marks, 4);
const char *name = "Ananya";
for (int i = 0; i < strlen(name); i++) {
int rounded = i;
(void) rounded;
}
long fee = 129900;
printf("Average %d, fee %d\n", rounded, fee);
return 0;
}gcc -std=c23 -Wall -Wextra -Wconversion -Wshadow flags.c -o flagsflags.c: In function 'main':
flags.c:16:19: warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion]
flags.c:18:23: warning: comparison of integer expressions of different signedness: 'int' and 'size_t' {aka 'long unsigned int'} [-Wsign-compare]
flags.c:19:13: warning: declaration of 'rounded' shadows a previous local [-Wshadow]
flags.c:16:9: note: shadowed declaration is here
flags.c:23:29: warning: format '%d' expects argument of type 'int', but argument 3 has type 'long int' [-Wformat=]
flags.c:13:14: warning: unused parameter 'argc' [-Wunused-parameter]
flags.c:13:26: warning: unused parameter 'argv' [-Wunused-parameter]This is GCC on 64-bit Linux, shortened: GCC also prints each source line with a marker under the problem. Each warning ends with the flag that controls it, which tells you what to search for in the documentation. Every one of them is worth fixing: the average 77.75 is silently cut to 77, the printf reads a long as an int (undefined behaviour), and the inner rounded hides the outer one, which is confusing at best.
What each warning flag covers
The names are misleading: -Wall does not enable all warnings. Here is what the common flags add for C, according to the GCC manual:
| Flag | Adds, among others | Why you want it |
|---|---|---|
-Wall | -Wformat, -Wuninitialized, -Wmaybe-uninitialized, -Wparentheses, -Wunused-variable | The basic safety net; always on |
-Wextra | -Wsign-compare (in C), -Wunused-parameter (with -Wall), -Wmissing-field-initializers, -Wimplicit-fallthrough | Catches signed/unsigned and switch fall-through bugs |
-Wpedantic (or -pedantic) | Warnings for anything that is not standard C | Keeps code portable between compilers |
-Wconversion | -Wfloat-conversion and, in C, -Wsign-conversion | Finds silent loss of value or sign |
-Wshadow | Variables that hide another variable | Prevents "wrong variable" bugs |
-Wformat=2 | -Wformat-nonliteral, -Wformat-security and more | Catches dangerous printf(user_text) calls |
A good everyday set for new code is -std=c23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Wformat=2. -Wconversion can be noisy in old code, so teams sometimes add it gradually, one folder at a time. Clang accepts all of these flags as well. Microsoft's compiler uses different names: /W4 is roughly its equivalent of a strict warning level.
Fixing warnings properly
A warning is fixed by making your intent clear, not by hiding it. Here is the corrected main:
#include <math.h>
#include <stdio.h>
#include <string.h>
double average(const int marks[], int n); // as before
int main(void) // no unused parameters
{
int marks[] = {72, 85, 64, 90};
int rounded = (int) lround(average(marks, 4)); // explicit, and rounded properly
const char *name = "Ananya";
for (size_t i = 0; i < strlen(name); i++) { // size_t matches strlen
// ...
}
long fee = 129900;
printf("Average %d, fee %ld\n", rounded, fee); // %ld for long
return 0;
}With lround from math.h the result is 78, the correct rounding of 77.75. On Linux, link with -lm when you use the maths library. If a function must keep a parameter it does not use (for example a callback with a fixed signature), C23 lets you mark it [[maybe_unused]], or write (void) param; in the body.
-Werror: making warnings fatal
-Werror turns every warning into an error, so the build fails until the warnings are fixed. It is very useful in a team's CI (continuous integration, the server that builds every change), because it stops new warnings from piling up. It can be annoying on your own machine, because a newer compiler with new warnings can break a build that worked yesterday. A common compromise is -Werror in CI only, or making just the most dangerous warnings fatal, for example -Werror=format-security -Werror=implicit-function-declaration.
Optimisation levels
| Flag | Meaning | Typical use |
|---|---|---|
-O0 | No optimisation (the default) | Stepping through code in a debugger |
-Og | Optimise only where debugging stays easy | Everyday development builds |
-O1, -O2 | Increasing optimisation; -O2 is the usual release level | Production builds |
-O3 | More aggressive, larger code; not always faster | Number crunching, after measuring |
-Os | Optimise for small size | Embedded firmware |
-g adds debug information and can be combined with any level; many teams ship -O2 -g builds and keep the debug information separately. Two warnings: some warnings, such as -Wmaybe-uninitialized, only appear with optimisation on, so build with both -O0 and -O2 regularly. And code with undefined behaviour often "works" at -O0 and breaks at -O2, as explained in Undefined Behaviour & Common Bugs. The optimiser is not wrong; the code is.
Settings and paths: -D, -I and -std
-D defines a macro from the command line, as if you had written #define at the top of every file. -I adds a folder to the list searched by #include "..." and #include <...>.
gcc -std=c23 -Wall -Wextra -Iinclude -DLOG_LEVEL=2 -DNDEBUG -O2 src/main.c src/log.c -o app// src/log.c: LOG_LEVEL comes from the command line, with a default
#ifndef LOG_LEVEL
#define LOG_LEVEL 1
#endif-DNDEBUG is special: it switches off every assert in the program, which is common in release builds. Always choose the language version with -std, as explained in C Standards, and consider -pedantic-errors if your code must build with several compilers.
Hardening flags for production
On Linux, extra flags make it harder for an attacker to exploit a bug such as a buffer overflow. They do not fix bugs, but they turn many silent memory corruptions into a clean crash.
-D_FORTIFY_SOURCE=2or=3makes the C library check the sizes of buffers passed to functions such asmemcpy,strcpyandsprintfwhen the compiler knows them. It needs optimisation (-O1or higher); level 3 needs a recent GCC and glibc.-fstack-protector-strongplaces a guard value on the stack in risky functions and stops the program if it is overwritten.-fPIE -piebuilds a position-independent program, so the operating system can load it at a random address.-fhardened, added in GCC 14 for GNU/Linux targets, enables a recommended set of these options in one flag, including the three above.
Check how your Linux distribution already configures its compiler: many enable some of these by default.
Common mistakes
- Believing
-Wallmeans all warnings. Add-Wextraand the extra flags above. - Silencing warnings with casts. A cast hides the message and keeps the bug. Fix the type or make the conversion deliberate and correct.
- Testing only at
-O0. Build and test the optimised version too. - Forgetting
-std. Each compiler version has its own default standard. - Using
-DNDEBUGin test builds. Your asserts disappear exactly when you need them.
Key points
- Turn on
-Wall -Wextra -Wpedantic, plus-Wconversion,-Wshadowand-Wformat=2for new code. - Each warning names its flag in square brackets; read it and fix the cause.
-Werrorstops warnings from piling up; it fits CI builds best.-O0/-Ogwith-gfor debugging,-O2for release,-Osfor small firmware.-Ddefines macros,-Iadds include folders,-DNDEBUGremoves asserts.- On Linux,
_FORTIFY_SOURCE,-fstack-protector-strong,-fPIE -pieor GCC's-fhardenedharden release builds.
Exercise
Compile this function in your head with -Wall -Wextra -Wconversion and list the warnings you expect. Then rewrite it so it compiles without warnings and returns the correct number of seats.
int seats_left(unsigned capacity, int booked, double cancelled_ratio)
{
int cancelled = booked * cancelled_ratio;
if (booked > capacity) {
return 0;
}
return capacity - booked + cancelled;
}Show solution
You should see warnings like these (the exact list varies a little between GCC versions): conversion from double to int may change value (-Wfloat-conversion, line 3); comparison of integers of different signedness (-Wsign-compare, line 4); and conversions between unsigned int and int that may change the sign of the result (-Wsign-conversion, line 7). A fixed version uses one signed type and an explicit, rounded conversion:
#include <math.h>
#include <stdio.h>
int seats_left(int capacity, int booked, double cancelled_ratio)
{
if (capacity < 0 || booked < 0 || booked > capacity) {
return 0;
}
int cancelled = (int) lround(booked * cancelled_ratio);
return capacity - booked + cancelled;
}
int main(void)
{
printf("%d\n", seats_left(72, 60, 0.1));
return 0;
}18
72 - 60 = 12 seats are free, and 10% of 60 bookings, 6, were cancelled, giving 18. Using int for every count removes the signed/unsigned mix, and lround makes the rounding visible.
Quick Quiz
1. Which statement about -Wall is true?
Despite its name, -Wall is only the starting point.
2. What does -Werror do?
With -Werror, a build with any warning stops, which keeps the code warning-free.
3. Which optimisation flag is meant to keep debugging easy while still optimising a little?
-Og applies only optimisations that do not get in the way of the debugger.
4. What does -DNDEBUG change?
assert is defined to do nothing when the macro NDEBUG is defined.
Learn C with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
