CModular Programming

Modular Programming

As a C program grows beyond a few hundred lines, cramming everything into a single .c file becomes unmanageable. Modular programming splits a program into multiple modules — pairs of files, each responsible for one coherent piece of functionality — that are compiled separately and linked together into a single executable.

The .c / .h Pair
  • A header file (.h) declares the module's public interface: function prototypes, type definitions, and constants that other modules are allowed to use.

  • A source file (.c) contains the actual implementation — function bodies, and any private (static) helpers that only this module needs.

  • Other modules #include the header to gain access to the interface, without needing to see (or recompile) the implementation.

Interface vs Implementation

This split mirrors a general engineering principle: separate what something does from how it does it. A header describes a contract — the function signatures and types callers can rely on — while the source file is free to change its internal implementation at any time, as long as it still fulfills that contract.

C
/* stack.h -- the public interface (the "what") */
#ifndef STACK_H
#define STACK_H

typedef struct Stack Stack;

Stack *stackCreate(int capacity);
void stackDestroy(Stack *s);
int stackPush(Stack *s, int value);
int stackPop(Stack *s, int *outValue);

#endif /* STACK_H */

C
/* stack.c -- the private implementation (the "how") */
#include <stdlib.h>
#include "stack.h"

struct Stack {
    int *items;
    int capacity;
    int top;
};

Stack *stackCreate(int capacity) {
    Stack *s = malloc(sizeof(Stack));
    if (s == NULL) {
        return NULL;
    }
    s->items = malloc(sizeof(int) * capacity);
    s->capacity = capacity;
    s->top = -1;
    return s;
}

void stackDestroy(Stack *s) {
    if (s != NULL) {
        free(s->items);
        free(s);
    }
}

int stackPush(Stack *s, int value) {
    if (s->top + 1 >= s->capacity) {
        return 0;
    }
    s->items[++(s->top)] = value;
    return 1;
}

int stackPop(Stack *s, int *outValue) {
    if (s->top < 0) {
        return 0;
    }
    *outValue = s->items[(s->top)--];
    return 1;
}
Note
Notice that `struct Stack` is only fully defined inside `stack.c`. The header only forward-declares `typedef struct Stack Stack;`, making it an **opaque type** — callers can hold a `Stack *` and call the public functions on it, but cannot see or depend on its internal fields. This is a common C technique for genuine encapsulation.
Using the Module from main.c

C
/* main.c -- a consumer of the stack module */
#include <stdio.h>
#include "stack.h"

int main(void) {
    Stack *s = stackCreate(10);

    stackPush(s, 1);
    stackPush(s, 2);
    stackPush(s, 3);

    int value;
    while (stackPop(s, &value)) {
        printf("Popped: %d\n", value);
    }

    stackDestroy(s);
    return 0;
}
Why Bother?

Benefit

Explanation

Compile time

Only the module you changed needs to be recompiled, not the whole program

Reusability

A well-designed module (like stack.c) can be dropped into other projects unchanged

Team collaboration

Different people can work on different modules with minimal file conflicts

Testability

A module with a clear interface can be tested in isolation, independent of the rest of the program

Readability

Smaller, focused files are far easier to understand than one giant file

A Typical Project Layout

Bash
project/
  main.c        # program entry point, ties modules together
  stack.h       # public interface for the stack module
  stack.c       # stack implementation
  queue.h       # public interface for the queue module
  queue.c       # queue implementation
  utils.h       # small shared helper functions
  utils.c
Tip
Once a project is split into multiple files like this, compiling each `.c` file individually and then linking them together becomes the natural next step — see Multi-File Projects for exactly how that build process works, and Build Basics with Make for automating it.
Warning
Always wrap header file contents in an **include guard** (`#ifndef HEADER_H` / `#define HEADER_H` / `#endif`) as shown above. Without it, including the same header from two different files in the same translation unit causes duplicate-definition compile errors.