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.
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 APIdynarray.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 binaryNow test incremental builds:
- Run
make— everything compiles. - Touch
dynarray.c:touch dynarray.c. - Run
makeagain — onlydynarray.oand the final link are redone. - Touch
dynarray.h. - Run
make— both.ofiles are recompiled.
Verify with valgrind
make valgrindZero 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
valgrindtarget as a CI-style check
The next module is Advanced pointers and memory — function pointers, void pointers, memory alignment, and a simple allocator.