Header Files & Compilation Units
Header Files & Compilation Units
As programs grow beyond a single file, you need to understand how C organises and links code across multiple source files.
The .h / .c Split
The convention in C is to separate declarations (what something is) from definitions (what something does):
- Header files (.h) contain declarations, type definitions, and macros that must be visible to multiple source files. They are
#included wherever needed. - Source files (.c) contain definitions — function bodies and variable storage. Each .c file is independently compiled into an object file (.o).
What Belongs in a Header
- Function prototypes:
int add(int a, int b); - Struct/union/enum type definitions:
typedef struct { ... } Point; - Macro definitions:
#define PI 3.14159 externvariable declarations:extern int g_count;(declares without allocating storage)static inlinefunction definitions (short enough to include in every translation unit)- Include guards to prevent multiple inclusion
What Belongs in a .c File
- Function definitions (the actual bodies)
- Variable definitions (which allocate storage):
int g_count = 0; staticfunctions and variables (file-scope only — not visible outside this .c file)
The extern Keyword
A global variable must be defined in exactly one .c file and declared (with extern) in the header:
/* config.c */
int g_debug_level = 0; /* definition — allocates storage */
/* config.h */
extern int g_debug_level; /* declaration — no storage allocated */The static Keyword (File Scope)
A function or variable declared static at file scope has internal linkage — it is invisible outside the current .c file. Use static for helpers that are implementation details not meant to be part of the public API:
static int helper(int x) { return x * 2; } /* invisible outside this .c file */The Compilation Model
main.c ──► (compiler) ──► main.o ──┐
math.c ──► (compiler) ──► math.o ──┼──► (linker) ──► program
utils.c ──► (compiler) ──► utils.o ──┘Each .c file is compiled independently. The linker resolves references between object files. This is why you can define a function in math.c and call it from main.c as long as main.c includes math.h (which declares the function prototype).
Forward Declarations
If two functions call each other (mutual recursion) in the same file, declare one before the other. For types, a forward declaration of a struct allows pointer declarations before the struct is fully defined:
struct Node; /* forward declaration — just the name */
struct Node *create_node(void); /* can use pointer to incomplete type */Circular Dependencies
Avoid circular includes (A.h includes B.h which includes A.h). Use forward declarations of struct types to break the cycle — pointers to an incomplete struct type are always valid since all pointers are the same size.
Code Examples
/* ===== math_utils.h ===== */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
/* Type definitions visible to all files that include this header */
typedef struct {
double x, y;
} Vec2;
/* Function declarations (prototypes) */
Vec2 vec2_add(Vec2 a, Vec2 b);
Vec2 vec2_scale(Vec2 v, double s);
double vec2_dot(Vec2 a, Vec2 b);
double vec2_length(Vec2 v);
void vec2_print(const char *label, Vec2 v);
#endif /* MATH_UTILS_H */
/* ===== math_utils.c ===== */
/* #include "math_utils.h" <- would be here in real files */
/* #include <stdio.h> <- would be here in real files */
/* #include <math.h> <- would be here in real files */
/* static: only visible within math_utils.c */
static double square(double x) {
return x * x;
}
Vec2 vec2_add(Vec2 a, Vec2 b) {
return (Vec2){ a.x + b.x, a.y + b.y };
}
Vec2 vec2_scale(Vec2 v, double s) {
return (Vec2){ v.x * s, v.y * s };
}
double vec2_dot(Vec2 a, Vec2 b) {
return a.x * b.x + a.y * b.y;
}
double vec2_length(Vec2 v) {
return sqrt(square(v.x) + square(v.y)); /* uses static helper */
}
void vec2_print(const char *label, Vec2 v) {
printf("%-10s = (%.3f, %.3f)\n", label, v.x, v.y);
}
/* ===== main.c ===== */
/* #include <stdio.h> <- would be here */
/* #include "math_utils.h" <- would be here */
/* Simulated inline for demonstration (all in one block for this example) */
#include <stdio.h>
#include <math.h>
typedef struct { double x, y; } Vec2;
static double square(double x) { return x * x; }
Vec2 vec2_add(Vec2 a, Vec2 b) { return (Vec2){a.x+b.x, a.y+b.y}; }
Vec2 vec2_scale(Vec2 v, double s){ return (Vec2){v.x*s, v.y*s}; }
double vec2_dot(Vec2 a, Vec2 b) { return a.x*b.x + a.y*b.y; }
double vec2_length(Vec2 v) { return sqrt(square(v.x)+square(v.y)); }
void vec2_print(const char *l, Vec2 v){ printf("%-10s = (%.3f, %.3f)\n",l,v.x,v.y); }
int main(void) {
Vec2 a = {3.0, 4.0};
Vec2 b = {1.0, 2.0};
vec2_print("a", a);
vec2_print("b", b);
vec2_print("a + b", vec2_add(a, b));
vec2_print("a * 2", vec2_scale(a, 2.0));
printf("dot(a,b) = %.3f\n", vec2_dot(a, b));
printf("length(a) = %.3f\n", vec2_length(a));
return 0;
}In a real project, math_utils.h would be a separate file with the include guard, math_utils.c would include that header and implement the functions, and main.c would include the header to get the declarations. The static square() helper is an implementation detail — callers outside math_utils.c cannot call it. To compile: gcc -Wall main.c math_utils.c -lm -o program
#include <stdio.h>
/* ---- Simulating two compilation units in one file for demonstration ----
In a real project these would be separate .c files.
config.h would contain:
extern int g_log_level; // declaration only
extern const char *g_app_name;
void config_init(int log_level, const char *name);
void config_print(void);
config.c would contain:
int g_log_level = 0; // DEFINITION (allocates storage)
const char *g_app_name = NULL;
void config_init(...) { ... }
*/
/* === config.c simulation === */
int g_log_level = 0; /* global variable — defined here */
const char *g_app_name = "MyApp";
void config_init(int level, const char *name) {
g_log_level = level;
g_app_name = name;
}
void config_print(void) {
printf("App: %s | Log level: %d\n", g_app_name, g_log_level);
}
/* === main.c simulation ===
In the real multi-file version main.c would have:
#include "config.h" <- gets the extern declarations
Then it can use g_log_level and g_app_name because the linker
resolves them to the definitions in config.o. */
extern int g_log_level; /* declaration — no new storage */
extern const char *g_app_name;
int main(void) {
config_print();
config_init(2, "ProductionApp");
config_print();
/* main.c can read/write the global via the extern declaration */
printf("Direct access: log_level=%d app=%s\n",
g_log_level, g_app_name);
g_log_level = 3;
printf("After increment: log_level=%d\n", g_log_level);
return 0;
}g_log_level is defined once in config.c (which allocates storage) and declared with extern in main.c (which just tells the compiler the variable exists somewhere). The linker connects both references to the same storage. If you put int g_log_level; in a header included by multiple .c files without extern, each .c file would define its own copy — a linker error.
#include <stdio.h>
/* === module_a.c simulation ===
These static functions are ONLY visible inside module_a.c.
They cannot be called from other .c files even if their
prototypes appeared in a header. */
static int clamp(int value, int lo, int hi) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
static int lerp_int(int a, int b, double t) {
return a + (int)((b - a) * t);
}
/* Public API function — no static, visible to other translation units */
int module_a_process(int raw_input) {
int clamped = clamp(raw_input, 0, 100); /* internal helper */
return lerp_int(0, 255, clamped / 100.0); /* internal helper */
}
/* === module_b.c simulation === */
/* module_b CANNOT call clamp() or lerp_int() — they are static in module_a.
But module_b can define its OWN static clamp() with different behaviour
without any name collision. */
static int clamp(int value, int lo, int hi) { /* different clamp — no conflict */
/* This version wraps around instead of clamping */
if (lo >= hi) return lo;
int range = hi - lo;
int v = (value - lo) % range;
if (v < 0) v += range;
return lo + v;
}
int module_b_wrap(int value) {
return clamp(value, 0, 10); /* uses module_b's own clamp */
}
/* === main.c simulation === */
int main(void) {
printf("module_a_process results (clamp 0-100 then scale to 0-255):\n");
int inputs[] = {-5, 0, 50, 100, 150};
for (int i = 0; i < 5; i++) {
printf(" input=%4d output=%3d\n",
inputs[i], module_a_process(inputs[i]));
}
printf("\nmodule_b_wrap results (wrapping clamp 0-10):\n");
int wraps[] = {-3, 0, 7, 10, 14, 23};
for (int i = 0; i < 6; i++) {
printf(" input=%4d output=%d\n",
wraps[i], module_b_wrap(wraps[i]));
}
return 0;
}static restricts a function's visibility to its translation unit (.c file). module_a and module_b both define a function called clamp — but because both are static, there is no naming conflict at link time. module_a_process is not static, so it is accessible from main.c through the linker. This is how C achieves encapsulation without namespaces.
Quick Quiz
1. What is the difference between a variable *declaration* and a variable *definition* in C?
2. What does the `static` keyword mean when applied to a function at file scope?
3. Which of the following should go in a header file (.h) rather than a source file (.c)?
4. Why is a forward struct declaration (`struct Node;`) useful when resolving circular header dependencies?
Was this lesson helpful?