Part of C in 100s

C in 100 Seconds: Header Files | Episode 16

Celest KimCelest Kim

Video: C in 100 Seconds: Three Files One Program — Header Files | Episode 16 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Header Files: Three Files, One Program

math_utils.h declares functions; math_utils.c defines them; main.c uses 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:

  1. First include: MATH_UTILS_H isn't defined → #ifndef is true → contents processed → #define sets the marker.
  2. Second include: MATH_UTILS_H is defined → #ifndef is 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.