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

extern int errno_copy; / double hypot(double, double);

Definition

A declaration that also creates the thing

int counter = 0; / int add(int a, int b) { return a + b; }

Statement

An action, inside a function body

x = f(y); / if (x) return 1; / { /* a block */ }

Expression

Something that produces a value

a + b * 2 / f(x) / p→field / (int)d

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.

point.h
#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 */
point.c
#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);
}
main.c
#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:

  1. Physical source characters are mapped to the source character set; trigraphs (removed in C23) were handled here.

  2. 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.

  3. Tokenization and comment removal — the file becomes preprocessing tokens; comments become whitespace.

  4. Preprocessing directives are executed and macros expanded, recursively including files. Only after this does the compiler proper see anything.

  5. Escape sequences in character constants and string literals are converted to the execution character set.

  6. Adjacent string literals are concatenated — "Hello, " "World" becomes one literal.

  7. Translation proper — syntax and semantic analysis, code generation.

  8. 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), and argv[argc] is guaranteed to be a null pointer — so argv can be walked without argc.

  • char *argv[] and char **argv are 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 } of main is equivalent to return 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_SUCCESS and EXIT_FAILURE from <stdlib.h> rather than bare integers; 0 is also guaranteed to mean success.

  • exit runs atexit handlers and flushes open streams; quick_exit runs at_quick_exit handlers instead; _Exit and abort skip both.

  • return from main and exit are equivalent — except that return destroys main’s automatic objects first, which matters if an `atexit handler could see them.

See Also