Syntax and the compiled toolchain
How the preprocessor, compiler and linker turn a .c file into a program, and the core syntax you write in between.
From source to executable
C is compiled ahead of time. A single gcc hello.c -o hello hides four stages; running them separately is the fastest way to understand where errors come from.
gcc -E hello.c -o hello.i # preprocess: #include and #define are expanded
gcc -S hello.i -o hello.s # compile: C becomes assembly
gcc -c hello.s -o hello.o # assemble: assembly becomes an object file
gcc hello.o -o hello # link: objects plus libraries become an executable
# in practice, one command, with the warnings turned up
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O2 hello.c -o hello| Flag | What it does |
|---|---|
-std=c17 | Choose the language revision (also c11, gnu17) |
-Wall -Wextra | Enable the warnings you actually want; never build without them |
-Werror | Treat warnings as errors so they cannot be ignored |
-g | Emit debug information for gdb and the sanitisers |
-O0 / -O2 | No optimisation, best for debugging / normal release optimisation |
-fsanitize=address,undefined | Runtime checks for memory errors and undefined behaviour |
-c | Compile to an object file and stop before linking |
-I / -L / -l | Add an include directory, a library directory, or link a library |
The core syntax
#include <stdio.h> /* declarations of the standard I/O functions */
#include <stdlib.h>
#include <stdint.h>
#define MAX_ITEMS 16 /* a macro: textual substitution, with no type checking */
/* a declaration tells the compiler the function exists */
static int sum(const int *values, size_t n);
int main(void) {
int32_t total = 0;
double rate = 0.075;
char grade = 'A';
const char *name = "Ada"; /* pointer to read-only text */
int values[MAX_ITEMS] = { 0 };
for (size_t i = 0; i < MAX_ITEMS; i++) {
values[i] = (int)i * 2;
}
total = sum(values, MAX_ITEMS);
printf("%s scored %d, grade %c, rate %.3f\n", name, total, grade, rate);
return EXIT_SUCCESS;
}
/* the definition provides the body */
static int sum(const int *values, size_t n) {
int acc = 0;
for (size_t i = 0; i < n; i++) acc += values[i];
return acc;
}- Types carry size and meaning:
charis 1 byte,intis usually 4, andsize_tis the unsigned type used for sizes and indices. printfformats by hand:%dfor int,%zuforsize_t,%ffor double,%cfor char,%sfor a string and%pfor a pointer.- A wrong length modifier or conversion is undefined behaviour, not a friendly warning: the call reads the wrong number of bytes.
- There is no
boolbefore C99; include<stdbool.h>or use an int where 0 means false. - Arrays do not know their own length, so pass the count alongside the pointer.
Reading compiler and linker output
$ gcc -Wall -Wextra -c demo.c
demo.c: In function 'main':
demo.c:12:13: warning: format '%d' expects argument of type 'int', but argument 2
has type 'const char *' [-Wformat=]
demo.c:20:5: error: 'count' undeclared (first use in this function)
# linker errors name symbols, not lines
$ gcc main.o util.o -o app
/usr/bin/ld: main.o: in function 'main':
main.c:(.text+0x2a): undefined reference to 'helper'
collect2: error: ld returned 1 exit status💡
A compile error concerns one translation unit and points at a line. A link error means a symbol was declared but never compiled in or never found:
undefined reference almost always means a missing object file or a missing -l library, not a missing declaration.FAQ
gcc or clang?
Either. Both implement the same standard, and both support the sanitisers; clang's diagnostics are often easier to read. Standards compliance, not the compiler name, is what makes code portable.
Why does the compiler accept code that crashes at run time?
C trusts the programmer. Bounds, lifetimes and type correctness are largely your responsibility, which is also why it is fast and predictable. Compile with
-Wall -Wextra and test under -fsanitize=address,undefined.Related
Pointers and memory Structs, files and undefined behaviour
Last refreshed 2026-09-18.