πŸ‡―πŸ‡΅ ζ—₯本θͺž | πŸ‡ΊπŸ‡Έ English
Advertisement

Multi-file C Projects

Move from a single .c file to a realistic header/source/Makefile layout.

Why split files

Once your code grows past ~1,000 lines, a single .c file becomes hard to read and slow to rebuild. Splitting brings several wins:

Header + source example

A tiny math library split across three files:
mathutil.h (header)
// declarations only
#ifndef MATHUTIL_H
#define MATHUTIL_H

int add(int a, int b);
int mul(int a, int b);
int factorial(int n);

#endif
mathutil.c (implementation)
#include "mathutil.h"

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

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

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
main.c (user)
#include <stdio.h>
#include "mathutil.h"

int main(void) {
    printf("2+3 = %d\n", add(2, 3));
    printf("4*5 = %d\n", mul(4, 5));
    printf("5! = %d\n", factorial(5));
    return 0;
}
Roles: the .h lists "what this module exports" (declarations). The .c is "how it actually works" (implementation). Readers should be able to use the module from the header alone.
#include "..." vs <...>: quotes for your own project's headers, angle brackets for standard library / external library headers.

Build flow & gcc options

gcc silently runs four stages internally: preprocess β†’ compile β†’ assemble β†’ link. Understanding this makes multi-file builds clearer.

Build all three in one go

$ gcc main.c mathutil.c -o app $ ./app 2+3 = 5 4*5 = 20 5! = 120

Split into stages (basis of incremental builds)

$ gcc -c main.c # compile only β†’ main.o $ gcc -c mathutil.c $ gcc main.o mathutil.o -o app # link
Next time, if only main.c changes, recompile just it and relink. That's what Make automates.

Common gcc flags

FlagMeaning
-cCompile only (no linking). Produces .o
-o nameOutput file name
-Wall -WextraEnable common warnings. Always use these.
-gInclude debug info (for gdb)
-O0 / -O2No optimization / normal release level
-std=c11 / -std=c17Select the C standard version
-I dirAdd a header search path
-L dirAdd a library search path
-l nameLink libname (e.g. -lm for math)
-D MACRODefine a preprocessor macro

include guards and extern

Why you need an include guard

If the same header is included twice, its contents get expanded twice and you get "redefinition" errors. The macro pattern below ensures it's expanded at most once per .c:
// top and bottom of mathutil.h
#ifndef MATHUTIL_H
#define MATHUTIL_H

/* header contents */

#endif
Modern alternative: gcc/clang support #pragma once β€” a single line that does the same thing. Not in the C standard, but widely portable.

Sharing a global variable (extern)

To use the same global from several .c files, put an extern declaration in the header (just a name + type) and the actual definition in exactly one .c file.
config.h
#ifndef CONFIG_H
#define CONFIG_H

extern int g_verbose;   // declaration only

#endif
config.c
#include "config.h"

int g_verbose = 0;      // the one and only definition
Don't put int g_verbose; in the header: every .c that includes it would get its own definition β†’ "multiple definition" linker error.

Makefile basics

The make tool reads a Makefile, detects which files changed, and rebuilds only what is needed.

Minimal Makefile

# Makefile β€” filename must start with capital M
# IMPORTANT: lines under a target must start with a TAB character

app: main.o mathutil.o
	gcc main.o mathutil.o -o app

main.o: main.c mathutil.h
	gcc -c main.c

mathutil.o: mathutil.c mathutil.h
	gcc -c mathutil.c

clean:
	rm -f *.o app
$ make gcc -c main.c gcc -c mathutil.c gcc main.o mathutil.o -o app $ make # nothing changed β†’ no work make: 'app' is up to date. $ make clean

Using variables & implicit rules

CC      = gcc
CFLAGS  = -Wall -Wextra -O2 -g
OBJS    = main.o mathutil.o
TARGET  = app

$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

# implicit rule handles .c β†’ .o automatically
main.o: main.c mathutil.h
mathutil.o: mathutil.c mathutil.h

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: clean
.PHONY: declare targets that aren't real files. Without it, a file named clean on disk would make make think the target is already done.

Auto-generated dependencies (advanced)

CFLAGS += -MMD
-include $(OBJS:.o=.d)
The gcc -MMD flag writes .d files with header dependencies, so make rebuilds whenever any included header changes.

Challenges

Challenge 1: Build the three-file project
Create mathutil.h / mathutil.c / main.c and a Makefile, run make, then ./app.
Challenge 2: Link the math library
Add a function in mathutil.c that uses sqrt() from <math.h>. Add -lm to LDFLAGS and make sure it links.
Challenge 3: Debug and release builds
Add two targets: make debug uses -g -O0, make release uses -O2.
Challenge 4: Reproduce a redefinition error
Remove the include guard from a header, include it from two places, and read the resulting error. Put the guard back and rebuild.