Program Structure & Compilation
Every C file follows the same skeleton, and every C program passes through the same four-stage journey from text file to running executable — understanding both up front makes everything else in this track click faster.
Two separate questions sit inside this page, and it helps to keep them apart from the start. First, what pieces is a C program actually built out of — functions, declarations, statements, blocks? Second, once you've written those pieces, what does the toolchain actually do to turn that plain text into a program your computer can run? This page walks through both, one numbered idea at a time, right through to what happens the instant you launch the finished executable.
The basic structure of a C program
A complete C program is assembled from a small, recognizable set of pieces, and once you can spot each one, no C file looks unfamiliar again.
Every one of the pieces this page is about to cover shows up somewhere in this one short file.
None of these terms need to make full sense yet — each one gets its own proper treatment in the sections that follow.
#include <stdio.h>
#define MAX_NUMBER 100
int global_value = 10;
void display_value(int value);
int main(void)
{
int number = 50;
display_value(number);
return 0;
}
void display_value(int value)
{
printf("Value = %d\n", value);
}C program
|
|-- preprocessor directives (#include, #define)
|-- global declarations (global_value)
|-- function declarations (display_value)
|-- the main() function
|-- other function definitions (display_value's body)
`-- statements and expressions, everywhere aboveA C program is made of functions
Functions are the primary way C organizes code. A small program might have only one; a real one has many, each handling one piece of the overall job.
This defines a function named add, taking two values a and b, and handing back their sum.
A larger program is really just more functions like this, each one a self-contained piece of a bigger problem broken down into manageable chunks.
int add(int a, int b)
{
return a + b;
}program
|-- main()
|-- add()
|-- subtract()
|-- calculate_average()
|-- print_result()
`-- ...The main() function
A hosted C program — the ordinary kind that runs under an operating system — always contains exactly one main function, and execution begins there.
main is a function in every technical sense — it follows the same rules any other function follows. The one thing that makes it special is that the C standard gives it a reserved role as the entry point of a hosted program; it isn't some separate kind of language construct.
#include <stdio.h>
int main(void)
{
printf("Hello\n");
return 0;
}program starts
|
main()
|
printf()
|
return 0
|
program terminatesFunction definition
A function definition is the piece of code that actually contains a function's real implementation.
Read together, this line and body say: define a function called add that takes two int parameters and hands back an int.
int add(int a, int b)
{
return a + b;
}int -> return type
add -> function name
(int a, int b) -> parameters
{ return a+b; } -> function bodyFunction declaration
Sometimes a function gets called before the compiler has seen its full definition yet — and that's exactly what a declaration is for.
The first line, int add(int a, int b);, is a function declaration. It tells the compiler "a function called add exists, it takes two ints, and it returns an int" — without yet showing what add actually does. The real implementation is free to appear further down the file.
int add(int a, int b);
int main(void)
{
int result = add(10, 20);
return 0;
}
int add(int a, int b)
{
return a + b;
}Declaration vs. definition
This one distinction underpins almost everything that comes later in this page, so it's worth pinning down precisely now.
A declaration tells the compiler that something exists and describes enough about it to use it correctly — for example, int add(int, int);. A definition goes further: it provides the actual implementation, or in the case of a variable, actually creates the storage for it — for example, the full int add(int a, int b) { return a + b; }.
Hold onto this distinction — it becomes essential once header files, multiple source files, and the extern keyword enter the picture later in this track, and it's exactly what separates a compiler error from a linker error, covered further down this page.
declaration -> "this exists"
definition -> "here is what it actually is / does"Statements
A statement is a single complete action in a C program.
Each of these lines is its own statement, and each one ends with a semicolon, which is how the compiler knows exactly where one instruction stops and the next one begins.
int x = 10;
x = x + 5;
return 0;
printf("%d", x);Expressions
An expression is a combination of operands and operators that produces a value — it computes something, even if that something is never stored anywhere.
Each of these is an expression: it evaluates to a value, but on its own, none of them is a complete statement.
Looking at int result = a + b;, the piece a + b is an expression, while the entire line — a declaration with an initializer — is the statement that contains it. Keeping expressions and statements separate in your head pays off once operators get more involved.
a + b
x * 10
a > bBlocks
A block is a group of declarations and statements enclosed in a matching pair of braces.
A function's body is itself just a block — the same braces-delimited grouping used everywhere else in C.
Blocks matter beyond just grouping code visually — they're also what defines scope, which is covered as its own topic once variables have been introduced properly.
{
int x = 10;
printf("%d\n", x);
x = 20;
}int main(void)
{
int x = 10;
return 0;
}Comments
A comment is text meant for a human reader; the compiler skips over it entirely, so it has no effect on how the program behaves.
Comments are meant to explain code that isn't obvious on its own — they're a supplement to clear structure, not a substitute for it.
// This is a single-line comment/*
This is
a multi-line comment.
*/#include <stdio.h>
int main(void)
{
// Print a message
printf("Hello\n");
return 0;
}Preprocessor directives
Lines beginning with # are preprocessor directives, and they're handled during a distinct step called preprocessing, before the compiler's own analysis of the program even starts.
The most common preprocessing facilities include #include, #define, #if, #ifdef, #ifndef, #else, #elif, and #endif, along with #undef for removing a previous definition.
Before real compilation happens, every occurrence of MAX_SIZE is textually replaced with 100 — this substitution step is called macro expansion, and it happens purely as text, with no understanding yet of C's actual grammar or meaning.
The preprocessor gets a full topic of its own later in this track — for now, the important idea is simply that it runs first, and it works on text, not on C's actual semantics.
#include <stdio.h>
#define MAX 100#define MAX_SIZE 100
int array_size = MAX_SIZE;Header files
A C program can pull in declarations that live in a separate file, called a header, using #include.
This particular header supplies the declarations behind the standard input/output facilities, including printf.
Headers commonly hold function declarations, type definitions, macros, constants, and other declarations meant to be shared. A program-specific header, included with quotes instead of angle brackets, works the same way:
A header like this might declare functions that are actually implemented over in calculator.c — a common project shape that looks like this:
Splitting declarations (the header) from implementation (the .c file) is exactly what lets a program grow across multiple files instead of staying in one.
#include <stdio.h>#include "calculator.h"project/
|-- main.c
|-- calculator.c
`-- calculator.hSource files and header files
By convention, C source files end in .c and header files end in .h.
This diagram is the first real look at where this page is heading — once program structure is covered, the rest of the page follows that exact path from source files down to a finished executable.
calculator.h
|
| declarations
v
main.c -----------+
|
calculator.c ------+
v
Compiler
v
Object files
v
Linker
v
ExecutableA more realistic C program
Putting the last few ideas together, a slightly more realistic project than a single main.c might look like this.
calculator.h — an include guard (the #ifndef / #define / #endif trio) prevents this header's contents from being pasted in twice if it's ever included more than once, directly or indirectly.
calculator.c — the actual implementation behind the declaration the header promised.
main.c — uses add() having only seen its declaration through the header; it doesn't need to know or care how add is implemented.
This three-file shape, however small, is already a genuine example of the structure real C projects use — far more representative than cramming everything into a single .c file.
#ifndef CALCULATOR_H
#define CALCULATOR_H
int add(int a, int b);
#endif#include "calculator.h"
int add(int a, int b)
{
return a + b;
}#include <stdio.h>
#include "calculator.h"
int main(void)
{
int result = add(10, 20);
printf("Result = %d\n", result);
return 0;
}calculator.h
|
|----------------> main.c
|
`----------------> calculator.c
main.c --------> main.o
calculator.c ---> calculator.o
main.o + calculator.o
|
v
Linker
|
v
ExecutableWhat is compilation?
With the pieces of a C program covered, the second half of this page turns to what actually happens to those pieces before the computer can run them. Compilation, in the broad sense, is the overall process of translating C source code into a form the target system can eventually execute.
People often use the word "compilation" loosely to mean this entire pipeline, but a real toolchain actually runs through several distinct stages to get there — each one covered individually below.
C source code
|
Preprocessing
|
Compilation
|
Assembly
|
Assembling
|
Object file
|
Linking
|
ExecutableThe complete C build process
Following one tiny program all the way through makes the pipeline concrete rather than abstract.
The next several sections each zoom into exactly one of these boxes.
#include <stdio.h>
int main(void)
{
printf("Hello\n");
return 0;
}main.c
|
v
[ Preprocessor ]
|
v
Preprocessed source
|
v
[ Compiler ]
|
v
Assembly
|
v
[ Assembler ]
|
v
main.o / main.obj
|
v
[ Linker ]
|
v
ExecutableStage 1 — preprocessing
The preprocessor handles #include and #define, along with the rest of the directives from section 11, performing purely textual and conditional transformations according to C's preprocessing rules.
It's worth being precise here: the preprocessor does not perform the compiler's semantic work. It has no concept of types, functions, or meaning — it only rewrites text before real compilation ever begins.
#include <stdio.h>
#define VALUE 100
int main(void)
{
printf("%d\n", VALUE);
return 0;
}main.c
|
Preprocessor
|
Expanded sourceStage 2 — compilation
The compiler proper takes the preprocessed source and actually makes sense of it.
Given a line like this, the compiler works through those stages to understand both its syntax and its meaning, and to generate lower-level code that carries out the same computation. The exact internal architecture varies between compilers, but this general shape — analyze, then generate — holds broadly.
Source
|
Lexical analysis
|
Parsing
|
Semantic analysis
|
Intermediate representation
|
Optimization
|
Code generationint x = 10 + 20;Stage 3 — assembly
One typical output of the compilation stage is assembly language — a form that's still recognizably readable, but tied to one specific processor architecture.
The same C source, compiled for two different processor families, produces two genuinely different pieces of assembly — this is the point in the pipeline where target-specific detail first appears.
C source
|
Compiler
|
AssemblyC
|
+-------+-------+
v v
ARM x86
v v
ARM assembly x86 assemblyStage 4 — assembling
The assembler's job is to turn that assembly language into an object file.
Object files typically carry the extension .o or .obj, depending on the platform and toolchain. Inside, an object file holds machine code, data, a symbol table, relocation information, and other bookkeeping metadata the linker will need next.
Assembly
|
Assembler
|
Object fileStage 5 — linking
Suppose one file calls a function that's actually implemented somewhere else.
If add() lives in calculator.c rather than in this file, then after compilation, each source file has become its own separate object file: main.c becomes main.o, and calculator.c becomes calculator.o.
The linker's job is to combine the necessary pieces, resolving references between them, until it can produce one finished, runnable executable.
int result = add(10, 20);main.o + calculator.o + required libraries
|
Linker
|
ExecutableWhy do we need a linker?
main.c — this file only has a declaration of add, not its implementation.
add.c — the actual implementation, compiled entirely separately.
main.o has a hole where add's actual code should be, and add.o has exactly what fills it. The linker is the piece of the toolchain that connects that reference to its definition — without it, two separately compiled files could never be joined into one working program.
int add(int a, int b);
int main(void)
{
return add(10, 20);
}int add(int a, int b)
{
return a + b;
}main.c -> main.o
add.c -> add.o
main.o
| "I need add()"
v
Linker
^
| "Here is add()"
|
add.oThe executable
Once linking finishes, the toolchain produces an executable, or more generally, a target-specific program image.
On a desktop operating system, that format depends on the platform: Windows uses PE, Linux uses ELF, and macOS uses Mach-O. Embedded development often targets other image formats instead — ELF, HEX, or BIN — depending on the specific toolchain and how the resulting program gets loaded or debugged onto the target device.
Compilation error
Not every attempt to compile succeeds — the compiler checks the source against C's actual rules along the way.
There's no value after the equals sign here, so this simply isn't valid C — the compiler will refuse to produce an object file from it and reports an error instead.
The only way forward from a compilation error is fixing the source and compiling again — nothing downstream in the pipeline ever runs.
int main(void)
{
int x = ;
return 0;
}Source code
|
Compiler
|
X Compilation error
|
No executable from this buildWarning vs. error
These sound similar but mean genuinely different things to a compiler.
An error means the compiler cannot successfully translate the program at all under the language and toolchain rules it's following — this line simply cannot be compiled.
This, by contrast, compiles just fine — but depending on the compiler's settings, it may still warn that x is declared and given a value but never actually used. A warning flags something suspicious or questionable, not something that stops compilation.
Warnings deserve real attention. Professional C development commonly compiles with aggressive warning settings turned on and treats most of what they report as worth fixing, not ignoring.
int main(void)
{
int x = ;
}int main(void)
{
int x = 10;
return 0;
}Compiler error vs. linker error
This distinction comes up constantly in interviews, and it flows directly from the declaration-versus-definition idea back in section 6.
With no declaration anywhere for unknown_function, the compiler can flag this as a source-level problem right away — a compiler-stage error.
This is a different situation: calculate has been properly declared, so the compiler is satisfied and produces object code for this file successfully. But if calculate is never actually defined anywhere in the program, linking fails afterward, because the linker can't find a body to connect the reference to.
The exact wording of that failure differs between toolchains, but the underlying cause is always the same: a declaration existed, but no matching definition was ever linked in.
int main(void)
{
unknown_function();
return 0;
}int calculate(void);
int main(void)
{
return calculate();
}Compiler
|
Source is acceptable enough to produce object code
|
Linker
|
X Undefined reference / unresolved symbolTranslation unit
This term comes up often enough in real C discussion that it's worth defining precisely.
A translation unit is, essentially, a single source file after preprocessing — including everything pulled in by its #include directives — right before it gets turned into object code.
After preprocessing, whatever header.h contains has been pasted directly into this file's text — together, they form one translation unit.
This concept becomes especially important once multi-file programs and header files get their own dedicated treatment later in this track.
#include "header.h"
int main(void)
{
return 0;
}main.c + included headers
|
Preprocessing
|
Translation unit
|
Compiler
|
Object fileOne .c file vs. multiple .c files
A beginner project is usually just main.c. As it grows, splitting it up starts paying off.
Each source file compiles into its own object file independently, and only at the end does the linker combine them into one program — this is exactly what gives a larger C project modularity and lets a build tool recompile only the files that actually changed.
project/
|-- main.c
|-- math.c
|-- math.h
|-- display.c
|-- display.h
`-- utility.cmain.c -> main.o
math.c -> math.o
display.c -> display.o
utility.c -> utility.o
|
v
Linker
|
v
ExecutableThe role of header files
The header supplies exactly what a caller of add needs to know to use it correctly; the source file supplies how it's actually implemented.
This is a solid mental model to start from, though real headers often hold more than just function declarations — type definitions, macros, and constants commonly live there too, as section 12 mentioned.
// math.h
int add(int a, int b);// math.c
int add(int a, int b)
{
return a + b;
}// main.c
#include "math.h"
int main(void)
{
return add(10, 20);
}.h -> "what is available?"
.c -> "how is it implemented?"What happens when you run the executable?
The very last step ties the whole page together: once you actually launch the finished program, on a typical hosted operating system, a fairly predictable sequence unfolds.
The operating system loads the executable into a new process, runs whatever startup work the runtime needs before your code begins, and finally hands control to main — the exact same function this page started with, now finally actually running.
A C program is a plain text file (or several) containing preprocessor directives, function declarations/definitions, and exactly one main() function. That text is turned into a runnable program by a compiler through four stages: preprocessing, compilation, assembly, and linking.
Once you can picture these four stages, compiler error messages stop being scary — a 'file not found' error is a preprocessing problem, a syntax error is a compilation problem, and an 'undefined reference' is a linking problem. Each stage fails in its own distinct way.
This exact pipeline (or a close variant of it) is what runs every time you type gcc file.c or click 'Build' in any C IDE — Code::Blocks, CLion, VS Code with a C extension, or a Makefile-driven project all funnel through the same four stages underneath.
Think of building a house from a blueprint: the preprocessor is like an assistant photocopying and pasting in reference pages your blueprint points to; the compiler is the architect translating the whole blueprint into a materials-and-steps list; the assembler turns that list into actual cut lumber and pipe; and the linker is the crew that bolts your house together with pre-built plumbing and electrical modules (libraries) you didn't build yourself.
A prototype and its definition This shows the two most common top-level pieces of a C file working together: a function prototype (a promise of what square looks like) declared before main, and the real definition given afterward.
The preprocessor runs first, expanding #include <stdio.h> into the full text of the standard I/O declarations.
The compiler reads the prototype 'int square(int n);' and remembers that square exists, takes an int, and returns an int — even though it hasn't seen the real definition yet.
Inside main, square(5) is called; the compiler already trusts this call because of the prototype it saw above.
The compiler then reaches the actual definition of square lower in the file and compiles its body: n * n.
At link time, the call to square inside main is connected to square's compiled machine code — even though they could, in a bigger project, live in entirely different files.
Without a prototype at all Moving square's definition below main but removing the prototype still works here, because the definition itself appears before it's actually called in translation order in this single file — but this only holds by luck of file layout, which is exactly why real programs use prototypes (usually via a header) instead of relying on ordering.
The compiler processes the file top to bottom; by the time it reaches main's call to square(5), it has NOT yet seen square's definition (which appears further down).
Modern C compilers require a visible declaration before a function is called — without one, this specific file actually fails to compile with an implicit-declaration error, which is precisely the point: relying on definition order alone is fragile.
The fix is always the same: put a prototype (or the whole definition) before any point that calls it — never assume the compiler will 'look ahead'.
int main() { int r = square(5); return 0; } int square(int n) { return n * n; } int square(int n); int main() { int r = square(5); return 0; } int square(int n) { return n * n; }
Calling square before the compiler has seen either its prototype or its full definition triggers an implicit-declaration error in modern compilers. Adding a prototype above main fixes it regardless of where the real definition ends up living.
Stage 1 — Preprocessing: handles every line starting with #, expanding #include files and #define macros into plain text, before the compiler proper ever sees the file.
Stage 2 — Compilation: translates the expanded C source into assembly language for your target CPU, catching every syntax and most type errors along the way.
Stage 3 — Assembly: an assembler turns that CPU-specific assembly text into an object file (.o or .obj) — real machine code, but not yet a runnable program.
Stage 4 — Linking: the linker combines your object file(s) with any libraries you use (like the C standard library, which supplies printf's actual implementation) into one final executable.
A function prototype (declaration) tells the compiler a function's name, return type and parameter types without providing its body — this is what lets you call a function before its full definition appears later in the file, or in another file entirely.
Calling a function before either its definition or a prototype has appeared — older compilers silently assumed int and often miscompiled; modern compilers correctly reject this as an error.
Confusing a compile-time error (your code is malformed) with a link-time error ('undefined reference to...' — the compiler understood you, but couldn't find the actual implementation anywhere).
Forgetting that #include only pastes in declarations, not the actual compiled implementation — that's what the linker supplies separately from a library.
Running gcc -E file.c to see exactly what your file looks like after preprocessing — an extremely useful way to debug a confusing macro.
Running gcc -S file.c to see the generated assembly, and gcc -c file.c to stop right after generating the object file, without linking.
Reading 'undefined reference to foo' as an instant signal to check whether foo's source file was actually compiled and linked into the build, not to go re-reading foo's own code for a syntax mistake.
Each .c file, after preprocessing, becomes one 'translation unit' — the actual, complete unit of text the compiler processes in one pass. Multi-file C programs compile each .c file into its own object file independently, which is exactly why the linker's job (stitching them back together) is a distinct, separate stage.
The C standard library is itself just precompiled object code printf isn't magic — it's a function whose compiled implementation lives inside a standard library file (like libc) that the linker automatically links against. This is the same mechanism you'll use later for your own multi-file programs and any third-party library.
List the four stages a C file passes through to become an executable.
Preprocessing, compilation, assembly, and linking — in that order.
What's the difference between a function declaration and a function definition?
A declaration (prototype) tells the compiler a function's signature without a body, letting you call it before its real code appears. A definition provides the actual body — the code that runs when the function is called.
You get 'undefined reference to main'. What does that tell you?
The linker couldn't find a compiled main function anywhere in the object files it was given — most often because the file defining main wasn't compiled or wasn't included in the build command at all.
Which of the four compilation stages would catch a missing semicolon?
Compilation — the compiler stage is what parses your C syntax and reports syntax errors like a missing semicolon.
What does a function prototype let you do that you couldn't do without it?
Call a function before its full definition appears later in the file (or in a different file entirely), because the compiler already knows its name, return type and parameter types.
Which stage combines your compiled code with the C standard library to produce a final executable?
Executable
|
Operating system
|
Process creation / loading
|
Program startup
|
main()
|
Program execution#include <stdio.h>
// function declaration (prototype)
int square(int n);
int main() {
int result = square(5);
printf("5 squared is %d\n", result);
return 0;
}
// function definition
int square(int n) {
return n * n;
}#include <stdio.h>
int main() {
int result = square(5);
printf("5 squared is %d\n", result);
return 0;
}
int square(int n) {
return n * n;
}