Program Structure
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A C program is a set of translation units — one per source file — each compiled independently and then
linked together. Understanding that boundary explains most of C’s apparent quirks: why headers exist, why
static means two different things, and why some errors appear only at link time.
The Grammar at a Glance
Almost everything in C is one of four things:
| Construct | What it is | Example |
|---|---|---|
Declaration |
Introduces a name and its type |
|
Definition |
A declaration that also creates the thing |
|
Statement |
An action, inside a function body |
|
Expression |
Something that produces a value |
|
A whole source file is nothing but a sequence of declarations and definitions at file scope; statements exist only inside function bodies.
#include <stdio.h> // preprocessor directive
enum { MAX_ITEMS = 64 }; // file-scope declaration (a constant)
static int item_count; // file-scope definition, internal linkage
static void report(void); // declaration -- promises a definition later
int main(void) // definition of a function
{
item_count = 3; // statement
report();
return 0;
}
static void report(void) // the promised definition
{
printf("%d of %d items\n", item_count, MAX_ITEMS);
}
Declarations vs. Definitions
The distinction is the one most worth getting right, because it is what headers are built on.
-
A declaration tells the compiler a name’s type so it can check uses of it. There may be many.
-
A definition allocates storage (for an object) or provides a body (for a function). There must be exactly one across the whole program.
extern int shared_total; // declaration: "exists somewhere, an int"
int shared_total = 0; // definition: this file owns the storage
double area(double r); // declaration (a prototype)
double area(double r) // definition
{
return 3.14159265358979323846 * r * r;
}
Two definitions of shared_total in two different translation units is a violation of the one-definition
rule; historically some linkers merged them silently, and modern ones reject it (GCC 10+ defaults to
-fno-common). A tentative definition — int shared_total; at file scope with no initializer — is C’s
special case: it becomes a definition initialized to zero if nothing else defines it.
See Storage Duration, Scope and Linkage for the full linkage rules.
Translation Units and Headers
The compiler sees one translation unit at a time: a source file with every #include textually pasted in and
every macro expanded. It has no notion of a project. So to use a function from another file you must declare
it — and to avoid writing the same declarations by hand in every file, you put them in a header and include
it.
#ifndef POINT_H // include guard: this file's contents appear once per TU
#define POINT_H
typedef struct Point {
double x;
double y;
} Point;
double point_norm(const Point *p); // declaration only -- no body here
extern const Point point_origin; // declaration only -- no storage here
#endif /* POINT_H */
#include "point.h"
#include <math.h>
const Point point_origin = { 0.0, 0.0 }; // the one definition
double point_norm(const Point *p) // the one definition
{
return sqrt(p->x * p->x + p->y * p->y);
}
#include "point.h"
#include <stdio.h>
int main(void)
{
Point p = { .x = 3.0, .y = 4.0 };
printf("|p| = %.1f, origin.x = %.1f\n", point_norm(&p), point_origin.x);
return 0;
}
$ clang -std=c23 -Wall -Wextra -o demo main.c point.c -lm
The rule of thumb: headers declare, source files define. Put in a header only what callers need — types,
prototypes, macros, extern declarations — and nothing that allocates storage or defines a body (unless it is
static or inline, see Functions).
The Translation Phases
The standard describes translation as eight ordered phases. Knowing the order explains real errors:
-
Physical source characters are mapped to the source character set; trigraphs (removed in C23) were handled here.
-
Line splicing — a backslash immediately before a newline joins the two lines. This is why a stray space after a
\in a multi-line macro breaks it. -
Tokenization and comment removal — the file becomes preprocessing tokens; comments become whitespace.
-
Preprocessing directives are executed and macros expanded, recursively including files. Only after this does the compiler proper see anything.
-
Escape sequences in character constants and string literals are converted to the execution character set.
-
Adjacent string literals are concatenated —
"Hello, " "World"becomes one literal. -
Translation proper — syntax and semantic analysis, code generation.
-
Linking — external references are resolved, producing the program image.
Phases 1-6 are the preprocessor’s world; see Preprocessor and Macros.
main and Its Signatures
A hosted C program starts at main. Only two signatures are guaranteed to be accepted:
int main(void)
{
return 0;
}
#include <stdio.h>
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; ++i) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
Facts worth knowing:
-
argv[0]is conventionally the program name (it may be empty), andargv[argc]is guaranteed to be a null pointer — soargvcan be walked withoutargc. -
char *argv[]andchar **argvare the same parameter type; array parameters decay to pointers. -
Implementations may accept other forms (
int main(int, char , char )for the environment on POSIX systems), but nothing else is portable. -
void main(void)is not standard C, whatever a compiler tolerates. -
Reaching the closing
}ofmainis equivalent toreturn 0;— a C99 rule, and the only function in C for which falling off the end is defined.
Exit Status
The value main returns (or the argument to exit) is the program’s exit status, truncated as if by
& 0xff on most POSIX systems.
#include <stdio.h>
#include <stdlib.h>
static void say_goodbye(void)
{
puts("cleaning up");
}
int main(void)
{
if (atexit(say_goodbye) != 0) {
return EXIT_FAILURE; // registration itself failed
}
FILE *f = fopen("/nonexistent/path", "r");
if (f == NULL) {
perror("fopen");
exit(EXIT_FAILURE); // runs atexit handlers, flushes streams
}
fclose(f);
return EXIT_SUCCESS; // 0
}
-
Use
EXIT_SUCCESSandEXIT_FAILUREfrom<stdlib.h>rather than bare integers;0is also guaranteed to mean success. -
exitrunsatexithandlers and flushes open streams;quick_exitrunsat_quick_exithandlers instead;_Exitandabortskip both. -
returnfrommainandexitare equivalent — except thatreturndestroysmain’s automatic objects first, which matters if an `atexithandler could see them.
See Also
-
Storage Duration, Scope and Linkage — what
staticandexternmean where. -
Preprocessor and Macros — include guards, conditional compilation, macro expansion.
-
Functions — prototypes,
inline, and variadic functions. -
Build and Tooling — compiling several translation units with Make and CMake.
-
C++: Program Structure and Compilation — the same model plus name mangling,
extern "C"linkage, and modules.
References
-
WG14 N3220 — the C23 working draft (§5.1.1.2 "Translation phases", §5.1.2.2.1 "Program startup", §6.7 "Declarations", §6.9 "External definitions").