C in 100 Seconds: Header Files | Episode 16
Video: C in 100 Seconds: Three Files One Program — Header Files | Episode 16 by Taught by Celeste AI - AI Coding Coach
C Header Files: Three Files, One Program
math_utils.hdeclares functions;math_utils.cdefines them;main.cuses them. The split between interface (header) and implementation (source).
C's "module" system is just text inclusion. Header files declare what's available; .c files implement; #include glues them together at compile time.
The three files
math_utils.h (the header — declarations):
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int square(int x);
int cube(int x);
#endif
math_utils.c (the implementation):
#include "math_utils.h"
int square(int x) {
return x * x;
}
int cube(int x) {
return x * x * x;
}
main.c (the user):
#include <stdio.h>
#include "math_utils.h"
int main() {
printf("square(5) = %d\n", square(5));
printf("cube(3) = %d\n", cube(3));
return 0;
}
Compiling
gcc main.c math_utils.c -o program
./program
# square(5) = 25
# cube(3) = 27
Pass both .c files to GCC. It compiles each independently, then links them together into one executable.
For larger projects you'd compile each .c into a .o (object file) separately, then link:
gcc -c main.c -o main.o
gcc -c math_utils.c -o math_utils.o
gcc main.o math_utils.o -o program
Same result. -c says "compile only, don't link." Useful for build systems that recompile only changed files.
What goes in a header
// declarations
int square(int x);
extern int counter;
// type definitions
struct Point { int x, y; };
typedef struct Point Point;
// constants
#define PI 3.14159
enum Color { RED, GREEN, BLUE };
// macros
#define MAX(a,b) ((a) > (b) ? (a) : (b))
The header advertises what the .c file provides. Anyone including it can use these.
What does NOT go in a header
// DON'T put function bodies in headers
int square(int x) { return x * x; } // BAD — every .c that includes this gets a copy
// DON'T put global variables (defined; declarations are OK)
int counter = 0; // BAD — multiple definitions cause linker error
Declarations only. The .c file owns the actual code and storage.
For small helpers that should be inlined, use static inline:
// In header:
static inline int double_it(int x) { return x * 2; }
static makes it file-local; inline hints to inline. Each .c file gets its own copy that's typically optimized away.
Include guards
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
// ... declarations ...
#endif
This is an include guard. Without it, if main.c includes math_utils.h twice (directly, or indirectly through another header), the declarations would appear twice — sometimes harmless, often a compile error (e.g., struct redefinition).
The pattern:
- First include:
MATH_UTILS_Hisn't defined →#ifndefis true → contents processed →#definesets the marker. - Second include:
MATH_UTILS_His defined →#ifndefis false → contents skipped.
Modern compilers also support #pragma once:
#pragma once
// ... declarations ...
Same effect, more concise. Slight portability concern — works on GCC, Clang, MSVC, but technically not standard. Both styles are common.
#include "..." vs <...>
#include <stdio.h> // angle brackets — system headers
#include "math_utils.h" // double quotes — your headers
The difference: search path order.
<stdio.h>searches the system include directories (/usr/include, etc.) only."math_utils.h"searches the current directory first, then system directories.
For your own project headers, use double quotes. For standard library or installed third-party libs, use angle brackets.
Forward declarations vs full includes
If header A only references struct B * (a pointer to B), it doesn't need the full definition of B — just a forward declaration:
// In a.h:
struct B; // forward declaration — "B exists; details elsewhere"
void process(struct B *b);
This is much faster to compile than #include "b.h" which might pull in hundreds of other declarations transitively.
For concrete uses (declaring a struct B value, accessing fields), you need the full definition.
Compile-time vs link-time
Headers handle the compile phase: each .c knows what other .cs offer. The compiler doesn't actually call those functions yet — it generates calls and leaves a placeholder.
Linking ties the placeholders to actual addresses. If you include the header but forget to link the .c:
gcc main.c -o program # forgot math_utils.c
# linker error: undefined reference to 'square'
The compile succeeds (because the prototype is in the header) but linking fails. The fix: include all relevant .c files in the build.
Real-world structure
project/
├── include/
│ ├── math_utils.h
│ ├── string_utils.h
│ └── data.h
├── src/
│ ├── main.c
│ ├── math_utils.c
│ ├── string_utils.c
│ └── data.c
├── tests/
│ └── test_math.c
└── Makefile
Headers in include/, sources in src/. Compile with -Iinclude so the compiler finds <...> includes.
For large projects, this is the start of a real build system (Make, CMake, Meson, etc.).
Common mistakes
Defining things in headers. Functions/globals defined in headers cause "multiple definition" linker errors when multiple .c files include them.
Forgetting include guards. Double-include compile errors.
Circular includes. A.h includes B.h includes A.h. Guards prevent infinite recursion, but the result depends on inclusion order.
Using forward declaration when you need full definition. struct B *b; is fine; struct B b; (declaration with size) needs the full type.
Not linking the .c. Linker can't find the implementation. "undefined reference to ..." errors. Pass all .c files to the compile command.
Headers that include other headers transitively. Include just what you need; long compile times come from "kitchen sink" includes.
What's next
Episode 17: pointers. The most powerful and most dangerous feature in C. Two ways to access the same memory.
Recap
Header (.h) declares functions, types, constants. Source (.c) defines them. Main file #includes headers and is compiled together with the relevant sources. Always use include guards (#ifndef ... #define ... #endif or #pragma once). Use angle brackets for system headers, double quotes for your own. Don't put function bodies or global variable definitions in headers — only declarations. For pointer-only references, forward-declare instead of including.
Next episode: pointers.