Code of the Day
AdvancedC compilation model

Lab: Multi-file project with a Makefile

Split a C program into multiple files with a proper header and build it with a Makefile — practising the full compilation model workflow.

Lab · optionalCAdvanced35 min

The best way to cement the compilation model is to actually split a program into multiple files and build it with make. In this lab you will take a working single-file program and restructure it into a proper multi-file C project.

Starting point

Begin with the dynamic array (DynArray) you built in the Memory Management lab. The goal is to split it into:

project/
├── Makefile
├── dynarray.h      # declarations only
├── dynarray.c      # implementations
└── main.c          # uses the DynArray API

dynarray.h

The header should contain only declarations:

#ifndef DYNARRAY_H
#define DYNARRAY_H

#include <stddef.h> /* for size_t */

typedef struct {
    int *data;
    int  len;
    int  capacity;
} DynArray;

void da_init(DynArray *da);
int  da_push(DynArray *da, int value);
int  da_get(const DynArray *da, int index);
int  da_len(const DynArray *da);
void da_free(DynArray *da);

#endif /* DYNARRAY_H */

dynarray.c

The implementation includes its own header and <stdlib.h>:

#include "dynarray.h"
#include <stdlib.h>
#include <stdio.h>

void da_init(DynArray *da) {
    da->data = NULL;
    da->len = 0;
    da->capacity = 0;
}

int da_push(DynArray *da, int value) {
    if (da->len == da->capacity) {
        int new_cap = (da->capacity == 0) ? 4 : da->capacity * 2;
        int *tmp = realloc(da->data, new_cap * sizeof(int));
        if (!tmp) { return -1; }
        da->data = tmp;
        da->capacity = new_cap;
    }
    da->data[da->len++] = value;
    return 0;
}

int da_get(const DynArray *da, int index) {
    if (index < 0 || index >= da->len) {
        fprintf(stderr, "da_get: index %d out of bounds\n", index);
        return -1;
    }
    return da->data[index];
}

int da_len(const DynArray *da) { return da->len; }

void da_free(DynArray *da) {
    free(da->data);
    da->data = NULL;
    da->len = da->capacity = 0;
}

main.c

Uses the API through the header:

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

int main(void) {
    DynArray arr;
    da_init(&arr);

    for (int i = 1; i <= 10; i++) {
        da_push(&arr, i * i);
    }

    printf("Array: ");
    for (int i = 0; i < da_len(&arr); i++) {
        printf("%d ", da_get(&arr, i));
    }
    printf("\nLength: %d\n", da_len(&arr));

    da_free(&arr);
    return 0;
}

Makefile

CC     = gcc
CFLAGS = -Wall -Wextra -g -std=c11
TARGET = dynarray_demo
OBJS   = main.o dynarray.o

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) $^ -o $@

main.o: main.c dynarray.h
	$(CC) $(CFLAGS) -c $< -o $@

dynarray.o: dynarray.c dynarray.h
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean valgrind
clean:
	rm -f $(OBJS) $(TARGET)

valgrind: $(TARGET)
	valgrind --leak-check=full ./$(TARGET)

Build and verify

make          # builds dynarray_demo
./dynarray_demo
make clean    # removes object files and binary

Now test incremental builds:

  1. Run make — everything compiles.
  2. Touch dynarray.c: touch dynarray.c.
  3. Run make again — only dynarray.o and the final link are redone.
  4. Touch dynarray.h.
  5. Run make — both .o files are recompiled.

Verify with valgrind

make valgrind

Zero errors, zero leaks.

Extension: add a sort function

Add void da_sort(DynArray *da) that sorts the array in ascending order using the standard library qsort. Add the declaration to dynarray.h, the definition to dynarray.c, and test it in main.c.

What you practised

  • Separating declarations (header) from definitions (source)
  • Writing include guards
  • A Makefile with pattern rules and automatic rebuild
  • Incremental compilation: only changed files are recompiled
  • The valgrind target as a CI-style check

The next module is Advanced pointers and memory — function pointers, void pointers, memory alignment, and a simple allocator.

Finished reading? Mark it complete to track your progress.

On this page