22 May 2025
Part of a series: Tooling for C
The first tool you need for C programming is a compiler. On Linux the main options are gcc and clang, both of which are open-source, stable, and have a functionality. I use clang -- mostly because that’s what I’m used to -- so in this article I’ll give an overview of how to use clang for C code. Most of the article should also apply to gcc, though, since clang was originally built to be gcc-compatible.
clang has a huge number of options. In this article, I’ll try to just cover what you need to get started.
Let’s take a look at a tiny sample project with three files, main.c, lib.h, and lib.c. We can compile the code with this command:
clang -o main main.c lib.c
This produces a file called main
that we can run directly:
./main
Hello, world!
In real projects C programmers usually compile their code in two steps. First, you compile each C file separately to an object file using the -c flag, then you link the object files to produce the executable:
clang -c -o main.o main.c
clang -c -o lib.o lib.c
clang -o main main.o lib.o
One flag you’ll almost always want to set is -std, which tells the compiler which version of C you’re using (manual). For the most recent version, C23, compile with:
clang -std=c23 …
Another feature you should almost certainly use is compiler warnings. If enabled, clang will analyze your code to detect certain mistakes and print a warning message for each. If you want to, you can enable and disable each check separately, but as a starting point I’d recomment compiling with
clang -Wall -Wextra …
You can also pass the -Werror flag, which turns warnings into errors, so compilation will fail as soon as there’s a single warning. This can be useful, for example, if you have a continuous integration setup and you want it to reject code if there are any warnings.
The manual has the full list: Diagnostic flags in Clang.
C projects typically have one set of flags for debug builds (for development and testing) and another for release builds (the ones the users will be running). Typical flags for a debug build are:
clang -O0 -g …
This disables optimizations and enables debug information, which you need if you want to use a debugger.
For a release build, you’ll want to enable optimizations:
clang -O2 …
This enables “most optimizations”. If you care a lot about performance, try compiling with -O3 (to enable additional optimizations for speed) and -Os (additional optimizations to reduce code size) and run some performance tests to see which works best for you code.
The last topic I want to cover is how you can run part of the compilation pipeline. We can think of a C compiler as consisting of four stages:
preprocessor -> compiler -> assembler -> linker
I already mentioned the -c flag above, which lets you run everything except the linker.
You can also run just the preprocessor using the -E flag:
clang -E -o lib_pp.c lib.c
clang-format -i lib_pp.c
I’ve included a clang-format command here to make the code more readable. If you ever find yourself debugging a complicated preprocessor macro, this will let you see exactly what the preprocessor does with your code.
To compile C to assembly, use the -S flag:
clang -S -o main.s main.c
For more in-depth info, run
man clang
or check out the user manual. In this article I’ve included links for version 20.1.0, the current stable release as of this writing. For other versions, follow the links on the LLVM Download Page or just change the version number in the URL.
I can also recommend the article My favorite C compiler flags during development by Chris Wellon.