Header Files and Multi-File Projects

So far, all your programs have lived in a single file. That works fine for small projects. But when your code grows beyond a few hundred lines, a single file becomes a nightmare.

Real programs are split into multiple files. Each file handles one piece of the puzzle. A game might have separate files for graphics, input, physics, and game logic. This keeps things organized and makes code easier to find, fix, and reuse.

This lesson teaches you how to split your code into multiple files and connect them together.

Why Split Code Into Multiple Files?

Three big reasons:

Organization. A 10,000-line file is impossible to navigate. But ten 1,000-line files, each with a clear purpose? That’s manageable. You know exactly where to look for the inventory code, the combat code, the save/load code.

Reuse. If your math functions live in their own file, you can use them in your next project. Just copy the file over. No need to hunt through a giant main.c and extract what you need.

Faster compilation. When you change one line in a 10,000-line file, the compiler must recompile all 10,000 lines. But if you change one line in a 500-line file, only that file gets recompiled. The other files stay as they were. For large projects, this saves serious time.

The Two Types of Files

C projects use two types of files:

Header files end in .h. They contain declarations - announcements of what exists. “There’s a function called add that takes two ints and returns an int.” They don’t contain the actual code.

Source files end in .c. They contain definitions - the actual implementation. “Here’s what add actually does.”

Think of it like a restaurant menu. The menu (header file) lists what’s available: “Burger - $10, Salad - $8.” But the menu doesn’t contain the actual food. The kitchen (source file) is where the real work happens.

What Goes in a Header File?

Header files contain declarations and other things that need to be shared across files:

  • Function declarations (prototypes) - what functions exist and their signatures
  • Type definitions - structs, enums, typedefs
  • Macros - #define constants and utility macros
  • Global variable declarations - using extern (we’ll cover this)

Header files do NOT contain:

  • Function implementations (the code inside functions)
  • Variable definitions (actually creating the variable)

Here’s a simple header file:

// math_utils.h

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// Constants
#define PI 3.14159265359

// Type definitions
typedef struct {
    double x;
    double y;
} Point;

// Function declarations (prototypes)
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double distance(Point p1, Point p2);

#endif // MATH_UTILS_H

Notice the function declarations end with semicolons. No curly braces, no code - just the signature.

What Goes in a Source File?

Source files contain the actual implementations:

// math_utils.c

#include "math_utils.h"
#include <math.h>  // For sqrt()

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

double distance(Point p1, Point p2) {
    double dx = p2.x - p1.x;
    double dy = p2.y - p1.y;
    return sqrt(dx * dx + dy * dy);
}

The source file includes its own header. This is important - it ensures the declarations and definitions match. If you make a typo, the compiler catches it.

Include Guards

You learned about include guards in Part 9 (The Preprocessor). They’re not optional for header files - they’re required.

Here’s the problem: what if two files both include your header?

// graphics.h
#include "math_utils.h"

// physics.h
#include "math_utils.h"

// main.c
#include "graphics.h"
#include "physics.h"  // math_utils.h gets included twice!

Without protection, math_utils.h gets pasted in twice. If it defines a struct, that struct gets defined twice. The compiler throws an error.

Include guards solve this:

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// ... header content ...

#endif // MATH_UTILS_H

Here’s how it works:

  1. First time the header is included: MATH_UTILS_H isn’t defined, so we enter the #ifndef block
  2. We immediately #define MATH_UTILS_H
  3. The header content gets processed
  4. Second time the header is included: MATH_UTILS_H is already defined, so the entire #ifndef block is skipped

The guard name should be unique. Convention is FILENAME_H in uppercase.

Every header file you write needs include guards. No exceptions.

Using Your Header

Other files use your code by including the header:

// main.c

#include <stdio.h>
#include "math_utils.h"

int main(void) {
    int sum = add(5, 3);
    printf("5 + 3 = %d\n", sum);

    Point p1 = {0.0, 0.0};
    Point p2 = {3.0, 4.0};
    printf("Distance: %.2f\n", distance(p1, p2));

    return 0;
}

Remember the difference:

  • #include <stdio.h> - angle brackets for system headers
  • #include "math_utils.h" - quotes for your own headers

Quotes tell the preprocessor to look in your project directory first.

Compiling Multiple Files

You can’t just compile main.c by itself anymore. The compiler will complain that it can’t find the add function - because the actual code is in math_utils.c.

Method 1: Compile everything at once

gcc main.c math_utils.c -o myprogram -lm

This compiles both source files and links them into one executable. The -lm links the math library (needed for sqrt).

Method 2: Compile 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 myprogram -lm

The -c flag means “compile only, don’t link.” This creates object files (.o files) - compiled but not yet combined.

The final command links the object files together into the executable.

Why bother with separate compilation? Speed. If you only changed main.c, you only recompile main.c:

gcc -c main.c -o main.o
gcc main.o math_utils.o -o myprogram -lm

The math_utils.o from before is reused. For big projects with dozens of files, this saves a lot of time.

A Complete Example

Let’s build a small project with multiple files. We’ll create a simple program that does math operations on points.

File Structure

my_project/
    main.c
    math_utils.h
    math_utils.c

math_utils.h

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// A 2D point
typedef struct {
    double x;
    double y;
} Point;

// Basic math operations
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);

// Point operations
Point point_add(Point p1, Point p2);
Point point_scale(Point p, double factor);
double point_distance(Point p1, Point p2);
void point_print(Point p);

#endif // MATH_UTILS_H

math_utils.c

#include "math_utils.h"
#include <stdio.h>
#include <math.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

Point point_add(Point p1, Point p2) {
    Point result;
    result.x = p1.x + p2.x;
    result.y = p1.y + p2.y;
    return result;
}

Point point_scale(Point p, double factor) {
    Point result;
    result.x = p.x * factor;
    result.y = p.y * factor;
    return result;
}

double point_distance(Point p1, Point p2) {
    double dx = p2.x - p1.x;
    double dy = p2.y - p1.y;
    return sqrt(dx * dx + dy * dy);
}

void point_print(Point p) {
    printf("(%.2f, %.2f)", p.x, p.y);
}

main.c

#include <stdio.h>
#include "math_utils.h"

int main(void) {
    // Test basic math
    printf("Basic Math:\n");
    printf("  5 + 3 = %d\n", add(5, 3));
    printf("  10 - 4 = %d\n", subtract(10, 4));
    printf("  6 * 7 = %d\n", multiply(6, 7));

    // Test point operations
    printf("\nPoint Operations:\n");

    Point p1 = {1.0, 2.0};
    Point p2 = {4.0, 6.0};

    printf("  p1 = ");
    point_print(p1);
    printf("\n");

    printf("  p2 = ");
    point_print(p2);
    printf("\n");

    Point sum = point_add(p1, p2);
    printf("  p1 + p2 = ");
    point_print(sum);
    printf("\n");

    Point scaled = point_scale(p1, 3.0);
    printf("  p1 * 3 = ");
    point_print(scaled);
    printf("\n");

    double dist = point_distance(p1, p2);
    printf("  Distance from p1 to p2: %.2f\n", dist);

    return 0;
}

Compiling and Running

gcc main.c math_utils.c -o points -lm
./points

Output:

Basic Math:
  5 + 3 = 8
  10 - 4 = 6
  6 * 7 = 42

Point Operations:
  p1 = (1.00, 2.00)
  p2 = (4.00, 6.00)
  p1 + p2 = (5.00, 8.00)
  p1 * 3 = (3.00, 6.00)
  Distance from p1 to p2: 5.00

Sharing Global Variables

Sometimes you need a variable accessible from multiple files. Use extern to declare it in the header, and define it in one source file:

globals.h:

#ifndef GLOBALS_H
#define GLOBALS_H

// Declaration - tells other files this variable exists
extern int game_score;
extern const char *player_name;

#endif

globals.c:

#include "globals.h"

// Definition - actually creates the variable
int game_score = 0;
const char *player_name = "Player 1";

main.c:

#include <stdio.h>
#include "globals.h"

int main(void) {
    game_score = 100;
    printf("%s has %d points\n", player_name, game_score);
    return 0;
}

The extern keyword says “this variable exists somewhere else - don’t create it here, just let me use it.”

Warning: Global variables should be used sparingly. They make code harder to understand and debug. Prefer passing data through function parameters when possible.

Static Functions

Sometimes you want a function that’s only used inside one file - a helper function that other files shouldn’t touch.

Mark it static:

// math_utils.c

#include "math_utils.h"

// Private helper - only visible in this file
static double square(double x) {
    return x * x;
}

// Public function - declared in header
double point_distance(Point p1, Point p2) {
    double dx = p2.x - p1.x;
    double dy = p2.y - p1.y;
    return sqrt(square(dx) + square(dy));
}

The square function is static, so it only exists within math_utils.c. Other files can’t call it, even if they tried to declare it. This keeps your implementation details private.

Project Organization Tips

As projects grow, organize your files into directories:

my_game/
    src/
        main.c
        game.c
        game.h
        graphics/
            render.c
            render.h
        input/
            keyboard.c
            keyboard.h
    include/
        common.h
    Makefile

For now, keeping all files in one directory is fine. As you build bigger projects, you’ll naturally want more structure.

Try It Yourself

  1. Create a string_utils.h and string_utils.c with functions to count vowels in a string, reverse a string, and check if a string is a palindrome. Use them from main.c.

  2. Take an existing single-file program you’ve written and split it into multiple files. Put related functions together.

  3. Create a vector.h and vector.c that implements a simple 3D vector (x, y, z) with operations for addition, subtraction, dot product, and magnitude.

  4. Make a small “library” of useful functions you’ve written throughout this series. Organize them into logical header/source pairs.

  5. Experiment with the compilation process: compile separately with -c, then link. Modify one file and see how you only need to recompile that one.

Common Mistakes

  • Forgetting include guards: Your code might work until something includes your header twice. Always add guards.

  • Putting function code in headers: Headers contain declarations, not definitions. If you put actual function code in a header and include it in multiple files, you’ll get “multiple definition” errors.

  • Including .c files: Never #include "something.c". Include headers, compile sources.

  • Mismatched declarations and definitions: If the header says int add(int a, int b) but the source says int add(int x, int y, int z), you’ll have problems. Include your own header in your source file to catch this.

  • Circular includes: If a.h includes b.h and b.h includes a.h, you’ve got a problem. Design your headers to avoid circular dependencies. Sometimes you need forward declarations instead of includes.

  • Forgetting to compile all source files: If you add a new .c file and forget to include it in your gcc command, you’ll get “undefined reference” errors.

  • Forgetting to link libraries: Functions like sqrt need -lm. Other libraries need their own flags.

The Pattern

Here’s the pattern for every module you create:

  1. Create the header (something.h):

    • Add include guards
    • Declare your types
    • Declare your functions
    • Add any constants or macros
  2. Create the source (something.c):

    • Include your own header first
    • Include any standard library headers you need
    • Implement your functions
  3. Use it (main.c or other files):

    • Include the header
    • Call the functions
  4. Compile:

    • List all .c files in the gcc command
    • Or compile separately and link

This scales from tiny projects to massive codebases. Professional C projects with millions of lines of code use exactly this pattern.

Next Up

In Part 26, we’ll learn about Make and build systems - automating your builds so you never have to type long compile commands again.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.