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

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 roughly 1,000 lines, a single .c file becomes hard to read and slow to rebuild. Splitting it up pays off in several ways:

Header + source example

Here's 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;
}
The roles: the .h lists what this module exports (declarations), while the .c contains how it actually works (implementation). A reader should be able to use the module from the header alone.
#include "..." vs. <...>: use quotes for your own project's headers, and angle brackets for standard or external library headers.

Build flow & gcc options

Internally, gcc quietly runs four stages: preprocess β†’ compile β†’ assemble β†’ link. Understanding these makes multi-file builds a lot clearer.

Build all three in one go

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

Splitting the stages (the 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 around, if only main.c has changed, you just recompile it and relink. That's exactly what make automates.

Common gcc flags

FlagMeaning
-cCompile only, don't link. Produces a .o file.
-o nameSets the output file name.
-Wall -WextraEnables the common warnings. Always use these.
-gIncludes debug info (for use with gdb).
-O0 / -O2No optimization / standard release-level optimization.
-std=c11 / -std=c17Selects the C standard version.
-I dirAdds a header search path.
-L dirAdds a library search path.
-l nameLinks libname β€” for example, -lm for the math library.
-D MACRODefines a preprocessor macro.

include guards and extern

Why you need an include guard

If the same header gets included twice, its contents are expanded twice and you end up with "redefinition" errors. The macro pattern below makes sure the header is expanded at most once per .c file:
// top and bottom of mathutil.h
#ifndef MATHUTIL_H
#define MATHUTIL_H

/* header contents */

#endif
Modern alternative: gcc and clang both support #pragma once β€” a single line that does the same job. It's not in the C standard, but it's widely portable.

Sharing a global variable with extern

To use the same global variable from several .c files, put an extern declaration in the header (just a name and 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, producing a "multiple definition" linker error.

Makefile basics

The make tool reads a Makefile, figures out which files have changed, and rebuilds only what's necessary.

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: declares targets that aren't real files. Without it, a file named clean sitting on disk would trick make into thinking the target is already up to date.

Auto-generated dependencies (advanced)

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

Challenges

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

Review Quiz

Check your understanding of this lesson!

Q1. What's the standard way to compile and link multiple .c files at once?

gcc main.c util.c -o app
gcc main.c + util.c
gcc -combine main.c util.c

List multiple .c files on the command line and gcc will compile each one and link them all together at the end.

Q2. What's the standard idiom to prevent double-inclusion of a header file?

Use include guards: #ifndef / #define / #endif
#pragma once_only
Define a #include_guard macro

Many compilers also support #pragma once, but for maximum portability the classic #ifndef guard is still the standard.

Q3. What's the basic form of a Makefile target and its dependencies?

target: dependencies on one line, commands on the next line prefixed with TAB
target = dependencies => command
[target] dependencies: command

Command lines must begin with a literal TAB character β€” this is a strict rule of make.