[C Series 11] Organizing Code with Separate Files and Headers

한국어 버전

Once you get comfortable grouping data with structs and enums, the next milestone is organizing code across multiple files. In C, the default pattern is to split function or struct definitions into several source files and collect their declarations in headers so other files can share them. This keeps you from stuffing every line into a single file and makes each file’s role explicit. In this post we will build the smallest possible two-file project to see exactly how .h and .c files divide responsibilities.

New Terms in This Post

  1. Header file: A .h file that carries declarations for functions, structs, and constants that other files need
  2. Declaration: A statement that tells the compiler a name and type exist
  3. Definition: The statement that provides the actual function body or storage for a variable
  4. Include guard: A preprocessor pattern that blocks duplicate declarations when the same header is included multiple times

Key Ideas

Study Notes

  • Time required: about 70 minutes
  • Prerequisites: structs, basic functions, experience running clang
  • Goal: separate headers and sources, distinguish declarations from definitions, understand multi-file build commands

Keep three things in mind as you read:

  1. Headers hold declarations, constants, typedefs—everything that describes the API—while .c files keep the actual implementation.
  2. Use the #ifndef/#define include guard pattern to prevent duplicate inclusions.
  3. Learn how to compile multiple .c files at once or as separate object files.

Code Walkthrough

Establishing the Project Layout

project/
├── main.c
├── stats.c
└── stats.h

stats.h lists the function declarations and any required structs. stats.c implements them, and main.c includes the header to call the functions. A declaration tells the compiler what exists; a definition provides the actual code or data.

  • stats.h: introduces the names and types
  • stats.c: performs the calculations
  • main.c: calls the functions and prints the results

Writing the Header

// stats.h
#ifndef STATS_H
#define STATS_H

#include <stddef.h>

typedef struct {
    double average;
    int max;
    int min;
} Stats;

Stats calculate_stats(const int *numbers, size_t length);

#endif

The file starts with #ifndef and #define, then ends with #endif. That pattern guarantees the header is processed only once even if multiple source files include it. The header contains declarations, not implementations.

Implementing the Functions

// stats.c
#include "stats.h"

Stats calculate_stats(const int *numbers, size_t length) {
    size_t i;
    Stats stats = {0.0, 0, 0};
    int total = 0;

    if (length == 0) {
        return stats;
    }

    stats.max = numbers[0];
    stats.min = numbers[0];

    for (i = 0; i < length; i++) {
        total += numbers[i];
        if (numbers[i] > stats.max) {
            stats.max = numbers[i];
        }
        if (numbers[i] < stats.min) {
            stats.min = numbers[i];
        }
    }

    stats.average = (double)total / (double)length;
    return stats;
}

Implementation files include the header to keep declarations and definitions aligned. If you change the function signature in one place but not the other, the compiler immediately throws an error.

Calling the Function from main.c

// main.c
#include <stdio.h>
#include "stats.h"

int main(void) {
    int numbers[5] = {72, 85, 90, 68, 77};
    Stats stats = calculate_stats(numbers, 5);

    printf("avg = %.1f\n", stats.average);
    printf("max = %d\n", stats.max);
    printf("min = %d\n", stats.min);

    return 0;
}

Use double quotes for project headers so the compiler searches the current project first. Use angle brackets (<...>) for installed standard headers like <stdio.h>.

Compiling Multiple Files

clang main.c stats.c -o stats-app

Once the project grows, you can compile each .c file separately into object files and then link them together. That is faster than rebuilding everything every time.

Or build object files separately for more control:

clang -c main.c -o main.o
clang -c stats.c -o stats.o
clang main.o stats.o -o stats-app

The second approach scales better as the project grows. When you edit one .c file you only need to recompile that translation unit; the other object files stay untouched. The combined command (clang main.c stats.c ...) works because stats.c contains the definition of calculate_stats, which main.c calls.

Declaring External Variables

// config.h
#ifndef CONFIG_H
#define CONFIG_H

extern int MAX_USERS;

#endif

// config.c
int MAX_USERS = 100;

When a global variable must be shared, put the extern declaration in the header and define the variable in exactly one .c file. extern does not allocate memory; it simply tells the compiler “this variable lives elsewhere.” This pattern prevents duplicate definition errors.

Why It Matters

Large programs quickly outgrow a single file. Splitting headers and sources lets teammates work on different areas simultaneously and lets the compiler build each unit independently, saving time. Because declarations and definitions are separate, developers can use a function by reading its header without diving into the implementation, making testing and documentation easier.

Practice in CodeSandbox

The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. Fork the starter and create a practice file such as hello.c
  2. Paste in the lesson code and compile it if clang or gcc is available
  3. Edit the code, rebuild it, and compare the new output

For C, the terminal build loop matters more than a browser preview. Compiler availability can vary by environment, so first confirm that clang or gcc is present in the Universal starter.

Practice

  • Try it: Recreate the stats.h/stats.c/main.c structure and compile using both approaches.
  • Extend it: Add a standard deviation field to Stats, then update both the declaration and definition to stay in sync.
  • Debug it: Remove the include guard on purpose, include the header twice, and observe the compiler errors.
  • Done when: You can explain the difference between declarations and definitions, describe what extern does, and write the command to compile multiple source files.

Wrap-up

You now know the standard pattern for splitting headers and implementation files. In the next post we will put those separate files to work by practicing standard input/output with stdio.h and reading and writing real data through files.

💬 댓글

이 글에 대한 의견을 남겨주세요