Header files and include guards
Write proper C header files — declarations vs. definitions, include guards, forward declarations, and the include search path.
- Explain the purpose of a header file
- Write a header file with an include guard
- Distinguish between what belongs in a header vs a source file
- Describe the include search path and the difference between <> and "" includes
A header file (.h) is not magic — it is a file of C declarations that the preprocessor pastes into any .c file that #includes it. The art of header files is knowing what to put in them and what not to, and preventing the same header from being included twice in the same translation unit.
What belongs in a header file
A header file should contain declarations — things that tell the compiler what exists, without creating any code or data:
/* math_utils.h */
/* Function prototypes */
double add(double a, double b);
double multiply(double a, double b);
int clamp(int value, int min, int max);
/* Type definitions */
typedef struct {
double x;
double y;
} Vector2;
/* Constants (as macros or const-qualified values) */
#define PI 3.14159265358979
extern const double E; /* declared here; defined in math_utils.c */
/* Enums */
typedef enum { NORTH, SOUTH, EAST, WEST } Direction;A header file should NOT contain:
- Function definitions (except
static inlinefunctions, a special case). - Variable definitions (
int counter = 0;). Useextern int counter;to declare; define in exactly one.cfile. - Any code that would run.
If a function definition is in a header and two .c files include that header, you get a "multiple definition" linker error.
Include guards
If the same header is included twice in one translation unit (directly or through other headers), its declarations are duplicated — causing "redefinition" errors. Include guards prevent this:
/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
/* ... declarations ... */
#endif /* MATH_UTILS_H */The first time this header is included, MATH_UTILS_H is not defined, so the preprocessor defines it and processes the content. The second time, MATH_UTILS_H is already defined, so everything between #ifndef and #endif is skipped.
The guard macro name must be unique — by convention it is the filename in uppercase with . replaced by _.
#pragma once — modern alternative:
#pragma once
/* ... declarations ... */Supported by all major compilers (gcc, clang, MSVC) but technically not standard. It is simpler and avoids naming collisions. Many codebases use it; some require traditional guards for strict portability.
Angle brackets vs. quotes
#include <stdio.h> /* system header: searched in standard include paths */
#include "math_utils.h" /* project header: searched relative to current file first */<> tells the preprocessor to search only the standard system include paths (/usr/include, /usr/local/include, etc.). "" searches the current file's directory first, then the standard paths. Use <> for system and library headers; use "" for your own headers.
Add directories to the include search path with -I:
gcc -I./include -I../common main.c -o mainA complete three-file example
vector.h — the public API:
#ifndef VECTOR_H
#define VECTOR_H
typedef struct {
double x;
double y;
} Vector2;
Vector2 vec_add(Vector2 a, Vector2 b);
double vec_length(Vector2 v);
#endifvector.c — the implementation:
#include "vector.h"
#include <math.h>
Vector2 vec_add(Vector2 a, Vector2 b) {
return (Vector2){ .x = a.x + b.x, .y = a.y + b.y };
}
double vec_length(Vector2 v) {
return sqrt(v.x * v.x + v.y * v.y);
}main.c — the user:
#include <stdio.h>
#include "vector.h"
int main(void) {
Vector2 a = {3.0, 0.0};
Vector2 b = {0.0, 4.0};
Vector2 sum = vec_add(a, b);
printf("Length: %.1f\n", vec_length(sum)); /* 5.0 */
return 0;
}Build:
gcc -Wall main.c vector.c -o main -lmBoth source files are compiled separately; the header provides the interface between them.
Forward declarations
Sometimes you need to refer to a struct before it is fully defined:
/* list.h */
struct Node; /* forward declaration: Node is a struct that will be defined later */
void process_list(struct Node *head); /* can use a pointer to an incomplete type */A pointer to an incomplete type is valid because all pointers are the same size. You cannot dereference through a forward-declared type (the compiler does not know its layout) — but you can pass pointers around.
Where to go next
Next: static and extern — controlling the visibility of functions and variables across compilation units.
The four stages: preprocessor, compilation, assembly, linking
Trace what gcc does in four distinct stages — preprocessing, compilation to assembly, assembly to object code, and linking — and understand each stage's inputs and outputs.
static and extern
Control symbol visibility across C translation units — using static to restrict visibility to one file and extern to share across files.