|$ curl https://forge-ai.dev/api/markdown?path=docs/c/multi-file
$cat docs/c-—-multi-file-programs.md
updated Recently·45 min read·published

C — Multi-file Programs

CHeadersMakefilesAdvancedAdvanced🎯Free Tools
Why Split Code Into Multiple Files

As programs grow beyond a few hundred lines, keeping everything in a single file becomes impractical. Multi-file projects improve organization, reduce compilation time, and enable code reuse across projects. C separates declarations from definitions, making it natural to split interfaces (headers) from implementations (source files).

main.c (bad — everything in one file)
C
1// A single-file project gets messy fast:
2// main.c — everything in one file (bad practice)
3
4#include <stdio.h>
5#include <stdlib.h>
6#include <string.h>
7#include <math.h>
8
9#define MAX_USERS 100
10
11typedef struct {
12 char name[64];
13 int age;
14 double score;
15} User;
16
17// User management functions
18User* create_user(const char* name, int age, double score) {
19 User* u = malloc(sizeof(User));
20 if (u) {
21 strncpy(u->name, name, 63);
22 u->name[63] = '\0';
23 u->age = age;
24 u->score = score;
25 }
26 return u;
27}
28
29void destroy_user(User* u) {
30 free(u);
31}
32
33// Database functions (unrelated to users)
34void db_connect(const char* url) {
35 printf("Connecting to %s\n", url);
36}
37
38void db_disconnect(void) {
39 printf("Disconnected\n");
40}
41
42// Logging functions
43void log_info(const char* msg) {
44 printf("[INFO] %s\n", msg);
45}
46
47int main(void) {
48 log_info("Starting application");
49 db_connect("postgres://localhost/mydb");
50 User* u = create_user("Alice", 30, 95.5);
51 if (u) {
52 printf("Created: %s\n", u->name);
53 destroy_user(u);
54 }
55 db_disconnect();
56 return 0;
57}
benefits.txt
TEXT
1Benefits of splitting into multiple files:
2
3 Organization Each file has a single responsibility
4 Compilation Only recompile files that changed
5 Reusability Headers can be included in other projects
6 Collaboration Multiple developers work on different files
7 Readability Each file is small and focused
Header Files (.h) and Source Files (.c)

Headers contain declarations — function prototypes, type definitions, macros, and extern variable declarations. Source files contain implementations — the actual function bodies and variable definitions. The header is the interface; the source file is the implementation.

include/user.h
C
1#ifndef USER_H
2#define USER_H
3
4#include <stddef.h>
5
6/* Maximum length for user name */
7#define USER_NAME_MAX 64
8
9/* User structure — complete type definition (not opaque) */
10typedef struct {
11 char name[USER_NAME_MAX];
12 int age;
13 double score;
14} User;
15
16/* Function prototypes — these are declarations, not definitions */
17User* user_create(const char* name, int age, double score);
18void user_destroy(User* u);
19int user_compare_by_score(const void* a, const void* b);
20void user_print(const User* u);
21
22/* This variable exists in exactly one .c file */
23extern int user_count;
24
25#endif /* USER_H */
src/user.c
C
1#include "user.h"
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5
6/* Definition of the global variable (only here) */
7int user_count = 0;
8
9User* user_create(const char* name, int age, double score) {
10 User* u = malloc(sizeof(User));
11 if (!u) return NULL;
12
13 strncpy(u->name, name, USER_NAME_MAX - 1);
14 u->name[USER_NAME_MAX - 1] = '\0';
15 u->age = age;
16 u->score = score;
17 user_count++;
18 return u;
19}
20
21void user_destroy(User* u) {
22 if (u) {
23 user_count--;
24 free(u);
25 }
26}
27
28int user_compare_by_score(const void* a, const void* b) {
29 const User* ua = (const User*)a;
30 const User* ub = (const User*)b;
31 if (ua->score < ub->score) return -1;
32 if (ua->score > ub->score) return 1;
33 return 0;
34}
35
36void user_print(const User* u) {
37 if (u) {
38 printf("User{name=%s, age=%d, score=%.1f}\n",
39 u->name, u->age, u->score);
40 }
41}

info

Headers describe what exists. Source files describe how it works. Never put function bodies in headers unless they are marked static inline.
How #include Works

The #include directive is a preprocessor command that performs text insertion. The preprocessor literally copies the contents of the named file into the current file at the point of inclusion. Two forms exist: angle brackets for system headers, quotes for project headers.

include_example.c
C
1/* Angle brackets: search system include paths */
2#include <stdio.h> /* /usr/include/stdio.h */
3#include <stdlib.h> /* /usr/include/stdlib.h */
4
5/* Quotes: search current directory first, then system paths */
6#include "user.h" /* ./user.h or ./include/user.h */
7
8/* The preprocessor expands this to the full contents of stdio.h */
9/* After preprocessing, all #include directives are replaced */
10/* with the actual content of the included files */
11
12/* You can verify with: gcc -E main.c -o main.i */
13/* The .i file shows the preprocessed output */
preprocessed_output.c
C
1/* What the preprocessor sees after expanding: */
2
3/* #include "user.h" becomes: */
4#ifndef USER_H
5#define USER_H
6#include <stddef.h>
7#define USER_NAME_MAX 64
8typedef struct { char name[USER_NAME_MAX]; int age; double score; } User;
9User* user_create(const char* name, int age, double score);
10void user_destroy(User* u);
11int user_compare_by_score(const void* a, const void* b);
12void user_print(const User* u);
13extern int user_count;
14#endif /* USER_H */
15
16/* The entire file content is pasted in place of the #include */
Include Guards and #pragma once

Without include guards, including the same header twice causes duplicate type definitions and linker errors. The #ifndef/#define/#endifpattern ensures a header's contents are processed only once per translation unit. #pragma once is a simpler, widely-supported alternative.

include/math_utils.h
C
1/* Traditional include guards — portable across all compilers */
2#ifndef MATH_UTILS_H
3#define MATH_UTILS_H
4
5double mu_sqrt(double x);
6double mu_pow(double base, double exp);
7int mu_factorial(int n);
8
9#endif /* MATH_UTILS_H */
10
11/*
12 * Without the guard:
13 * If a.c includes "user.h" and "math_utils.h",
14 * and "user.h" also includes "math_utils.h",
15 * then math_utils.h is included twice → duplicate declarations.
16 *
17 * The guard ensures the second inclusion is skipped entirely.
18 */
include/math_utils_pragma.h
C
1/* Modern alternative: #pragma once */
2/* Supported by GCC, Clang, MSVC, and most modern compilers */
3/* Not part of the C standard, but universally supported */
4
5#pragma once
6
7double mu_sqrt(double x);
8double mu_pow(double base, double exp);
9int mu_factorial(int n);
10
11/*
12 * #pragma once advantages:
13 * - Simpler to write and maintain
14 * - No risk of guard name collisions
15 * - Slightly faster (file-level skip)
16 *
17 * Include guards advantages:
18 * - Standard C (portable to every compiler)
19 * - Can be used for conditional compilation
20 * - Works with symlinks and hardlinks
21 *
22 * Both work. Pick one convention and stick with it.
23 */
extern and static: Controlling Visibility

externdeclares a variable or function defined elsewhere — it says "this exists in another file." static at file scope restricts visibility to the current translation unit — the linker cannot see it from other files.

include/counter.h
C
1/* counter.h */
2#ifndef COUNTER_H
3#define COUNTER_H
4
5extern int global_counter; /* Declaration — defined in counter.c */
6extern void counter_increment(void);
7extern int counter_get(void);
8
9#endif /* COUNTER_H */
src/counter.c
C
1/* counter.c */
2#include "counter.h"
3
4int global_counter = 0; /* Definition — storage allocated here */
5
6/* static limits this function to counter.c only */
7static void log_change(const char* action) {
8 printf("[counter] %s — value is now %d\n", action, global_counter);
9}
10
11void counter_increment(void) {
12 global_counter++;
13 log_change("incremented");
14}
15
16int counter_get(void) {
17 return global_counter;
18}
src/main.c
C
1/* main.c */
2#include <stdio.h>
3#include "counter.h"
4
5int main(void) {
6 /* Can access global_counter because it is declared extern in counter.h */
7 printf("Initial: %d\n", counter_get());
8
9 counter_increment();
10 counter_increment();
11 counter_increment();
12
13 printf("Final: %d\n", counter_get());
14 /* Output:
15 * [counter] incremented — value is now 1
16 * [counter] incremented — value is now 2
17 * [counter] incremented — value is now 3
18 * Final: 3
19 */
20
21 /* Cannot call log_change() — it is static in counter.c */
22 /* The linker would produce: undefined reference to 'log_change' */
23
24 return 0;
25}
KeywordScopeLinkageUsage
externVisible everywhereExternal linkageDeclare globals/functions defined in another file
staticCurrent file onlyInternal linkageHide functions/variables from the linker
static inlineCurrent file onlyInternal linkageSmall functions safe to put in headers
(none)Visible everywhereExternal linkageDefault for functions and file-scope variables
Compilation Model: Separate Compilation and Linking

C uses a multi-phase build process. Each .c file is compiled independently into an object file (.o), then the linker combines all object files into a single executable. This is why functions can be declared in headers but defined in separate source files.

build.sh
Bash
1# Step 1: Compile each source file to an object file
2gcc -c src/user.c -o build/user.o -Wall -Wextra -I include
3gcc -c src/counter.c -o build/counter.o -Wall -Wextra -I include
4gcc -c src/main.c -o build/main.o -Wall -Wextra -I include
5
6# Step 2: Link all object files into an executable
7gcc build/user.o build/counter.o build/main.o -o myapp -lm
8
9# Equivalent single-step compilation:
10gcc src/user.c src/counter.c src/main.c -o myapp -I include -Wall -Wextra
11
12# The -c flag tells gcc to compile but NOT link
13# The -I flag adds a directory to the header search path
14# The -Wall -Wextra flags enable useful warnings
linking_explained.c
C
1/* How the linker resolves cross-file references:
2 *
3 * user.c contains: User* user_create(...) { ... }
4 * main.c contains: extern User* user_create(...);
5 *
6 * After compilation:
7 * user.o — contains the symbol "user_create" (defined)
8 * main.o — references the symbol "user_create" (undefined)
9 *
10 * The linker matches undefined references with defined symbols
11 * across all object files. If a symbol is undefined and not found,
12 * you get: "undefined reference to 'user_create'"
13 *
14 * If a symbol is defined in multiple object files:
15 * "multiple definition of 'user_create'"
16 *
17 * This is why: declarations (extern) go in headers,
18 * definitions go in exactly one .c file.
19 */
Essential gcc Flags

Knowing the right compiler flags is critical for multi-file projects. The flags control warning levels, output format, include paths, and library linking.

FlagPurposeExample
-cCompile only (no linking)gcc -c main.c
-o nameSet output file namegcc -o myapp main.o
-Wall -WextraEnable comprehensive warningsgcc -Wall -Wextra *.c
-I dirAdd header search directorygcc -I include/ src/*.c
-L dirAdd library search directorygcc -L lib/ -lmylib
-l libLink against librarygcc -lm -lpthread
-gInclude debug symbolsgcc -g -o debug main.c
-O2 / -O3Optimization levelgcc -O2 -o fast main.c
-std=c11Set C standard versiongcc -std=c11 main.c
-pedanticStrict ISO compliance warningsgcc -pedantic main.c
-EPreprocess only (show expanded macros)gcc -E main.c -o main.i
Creating and Using Static Libraries

A static library (.a on Unix, .lib on Windows) is an archive of object files. At link time, the linker copies only the object files needed from the library into the final executable. The code becomes part of the binary.

build_static_lib.sh
Bash
1# Create object files from source
2gcc -c src/mathutils.c -o build/mathutils.o -I include -Wall
3gcc -c src/stringutils.c -o build/stringutils.o -I include -Wall
4
5# Create a static library (archive the .o files)
6ar rcs lib/libutils.a build/mathutils.o build/stringutils.o
7
8# ar = archiver, rcs = replace, create, index
9# The library name convention: lib<name>.a
10
11# Use the library in your project
12gcc src/main.c -o myapp -I include -L lib -lutils -Wall
13
14# -L lib → search for libraries in the lib/ directory
15# -lutils → link against libutils.a (note: no "lib" prefix or ".a" suffix)
16# The linker finds libutils.a and pulls in needed object files
ar_commands.sh
Bash
1# Useful ar commands
2ar t libutils.a # List contents (table of .o files)
3ar d libutils.a foo.o # Delete an object file from the archive
4ar x libutils.a # Extract all object files
5ar v rcs libutils.a *.o # Verbose mode — shows what it does
6
7# You can also use ranlib to generate an index
8ranlib libutils.a # Creates/updates the archive symbol index
9# ar rcs already runs ranlib, so this is usually redundant
Shared (Dynamic) Libraries

Shared libraries (.so on Linux, .dylib on macOS) are loaded at runtime rather than link time. Multiple programs can share a single copy of the library in memory. Code must be compiled as position-independent (-fPIC).

build_shared_lib.sh
Bash
1# Create a shared library
2gcc -shared -fPIC src/mathutils.c -o lib/libmathutils.so -I include -Wall
3
4# -shared → produce a shared library
5# -fPIC → position-independent code (required for shared libs)
6
7# On macOS, use .dylib extension:
8gcc -shared -fPIC src/mathutils.c -o lib/libmathutils.dylib -I include
9
10# Link against the shared library
11gcc src/main.c -o myapp -I include -L lib -lmathutils -Wall
12
13# Run — the dynamic linker must find the library at runtime
14export LD_LIBRARY_PATH=lib:$LD_LIBRARY_PATH # Linux
15export DYLD_LIBRARY_PATH=lib:$DYLD_LIBRARY_PATH # macOS
16./myapp
17
18# Inspect dynamic dependencies
19ldd myapp # Linux — shows which .so files are loaded
20otool -L myapp # macOS — same purpose

warning

Shared libraries require the library file to be present at runtime. If libmathutils.sois not in the library search path, you get a runtime error: "error while loading shared libraries: libmathutils.so: cannot open shared object file."
Makefile Basics

A Makefile defines build rules with targets, dependencies, and recipes. Make only rebuilds targets whose dependencies have changed, making incremental builds fast. This is essential for multi-file projects where recompiling everything is wasteful.

Makefile
MAKEFILE
1# Makefile for a multi-file C project
2
3# Compiler and flags
4CC = gcc
5CFLAGS = -Wall -Wextra -std=c11 -I include
6LDFLAGS = -lm
7
8# Directories
9SRCDIR = src
10INCDIR = include
11BUILDDIR = build
12BINDIR = bin
13
14# Source and object files
15SOURCES = $(wildcard $(SRCDIR)/*.c)
16OBJECTS = $(patsubst $(SRCDIR)/%.c, $(BUILDDIR)/%.o, $(SOURCES))
17TARGET = $(BINDIR)/myapp
18
19# Default target
20all: $(TARGET)
21
22# Link object files into executable
23$(TARGET): $(OBJECTS) | $(BINDIR)
24 $(CC) $(OBJECTS) -o $(TARGET) $(LDFLAGS)
25
26# Compile .c to .o (pattern rule)
27$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
28 $(CC) $(CFLAGS) -c $< -o $@
29
30# Create directories if they don't exist
31$(BINDIR):
32 mkdir -p $(BINDIR)
33
34$(BUILDDIR):
35 mkdir -p $(BUILDDIR)
36
37# Clean build artifacts
38clean:
39 rm -rf $(BUILDDIR) $(BINDIR)
40
41.PHONY: all clean
Makefile variables explained
MAKEFILE
1# Makefile automatic variables:
2
3# $@ = the target (the file being built)
4# $< = the first dependency (the source file)
5# $^ = all dependencies
6
7# Example pattern rule:
8$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
9 $(CC) $(CFLAGS) -c $< -o $@
10# $< = src/foo.c (first prerequisite)
11# $@ = build/foo.o (target)
12
13# Variable expansion:
14SOURCES = src/main.c src/user.c src/utils.c
15# $(SOURCES) expands to: src/main.c src/user.c src/utils.c
16
17# Wildcard patterns:
18SOURCES = $(wildcard src/*.c)
19OBJECTS = $(patsubst src/%.c, build/%.o, $(SOURCES))
20# If src/ has main.c user.c → SOURCES = src/main.c src/user.c
21# OBJECTS = build/main.o build/user.o
Makefile for library
MAKEFILE
1# A practical Makefile for a library project
2
3CC = gcc
4CFLAGS = -Wall -Wextra -std=c11 -Iinclude
5AR = ar
6ARFLAGS = rcs
7
8SRC_DIR = src
9INC_DIR = include
10BUILD = build
11LIB_DIR = lib
12
13SOURCES = $(wildcard $(SRC_DIR)/*.c)
14OBJECTS = $(patsubst $(SRC_DIR)/%.c, $(BUILD)/%.o, $(SOURCES))
15
16# Library targets
17STATIC_LIB = $(LIB_DIR)/libutils.a
18SHARED_LIB = $(LIB_DIR)/libutils.so
19
20.PHONY: all static shared clean test
21
22all: static shared
23
24static: $(STATIC_LIB)
25shared: $(SHARED_LIB)
26
27$(STATIC_LIB): $(OBJECTS) | $(LIB_DIR)
28 $(AR) $(ARFLAGS) $@ $^
29
30$(SHARED_LIB): $(OBJECTS) | $(LIB_DIR)
31 $(CC) -shared -fPIC $^ -o $@
32
33$(BUILD)/%.o: $(SRC_DIR)/%.c | $(BUILD)
34 $(CC) $(CFLAGS) -c $< -o $@
35
36$(BUILD) $(LIB_DIR):
37 mkdir -p $@
38
39clean:
40 rm -rf $(BUILD) $(LIB_DIR)
Multi-file Project Structure

A clean project structure separates public headers from private headers, source files from build artifacts, and tests from production code. This convention is widely used in C projects.

project_structure.txt
TEXT
1myproject/
2├── include/ Public headers (API interface)
3│ ├── user.h
4│ └── mathutils.h
5├── src/ Source files (implementation)
6│ ├── user.c
7│ ├── mathutils.c
8│ └── main.c
9├── tests/ Test files
10│ ├── test_user.c
11│ └── test_mathutils.c
12├── lib/ Built libraries (output)
13│ ├── libutils.a
14│ └── libutils.so
15├── build/ Object files (output)
16│ ├── user.o
17│ ├── mathutils.o
18│ └── main.o
19├── bin/ Executables (output)
20│ └── myapp
21├── Makefile
22└── README.md
23
24# Key conventions:
25# include/ → public headers (what other projects see)
26# src/ → implementation files
27# build/ → object files (gitignored)
28# lib/ → static/shared libraries (gitignored)
29# bin/ → executables (gitignored)
30# tests/ → test source files
Complete Project Example

A complete multi-file project with a string utility library, main program, and test file. This demonstrates all concepts: headers, source files, extern, static, compilation, and linking.

include/strutils.h
C
1#ifndef STRUTILS_H
2#define STRUTILS_H
3
4#include <stddef.h>
5
6/* Count occurrences of a character in a string */
7size_t str_count_char(const char* s, char c);
8
9/* Reverse a string in place */
10char* str_reverse(char* s);
11
12/* Check if string is a palindrome (case-insensitive) */
13int str_is_palindrome(const char* s);
14
15/* Split string by delimiter, returns array of strings */
16char** str_split(const char* s, char delim, size_t* count);
17
18/* Free the array returned by str_split */
19void str_split_free(char** parts, size_t count);
20
21/* Trim whitespace from both ends (returns pointer into s) */
22char* str_trim(char* s);
23
24#endif /* STRUTILS_H */
src/strutils.c
C
1#include "strutils.h"
2#include <ctype.h>
3#include <stdlib.h>
4#include <string.h>
5
6/* File-scope helper — invisible to other translation units */
7static char to_lower(char c) {
8 return (char)tolower((unsigned char)c);
9}
10
11size_t str_count_char(const char* s, char c) {
12 size_t count = 0;
13 for (; *s; s++) {
14 if (*s == c) count++;
15 }
16 return count;
17}
18
19char* str_reverse(char* s) {
20 if (!s) return NULL;
21 size_t len = strlen(s);
22 for (size_t i = 0; i < len / 2; i++) {
23 char tmp = s[i];
24 s[i] = s[len - 1 - i];
25 s[len - 1 - i] = tmp;
26 }
27 return s;
28}
29
30int str_is_palindrome(const char* s) {
31 if (!s) return 0;
32 size_t len = strlen(s);
33 for (size_t i = 0; i < len / 2; i++) {
34 if (to_lower(s[i]) != to_lower(s[len - 1 - i])) {
35 return 0;
36 }
37 }
38 return 1;
39}
40
41char** str_split(const char* s, char delim, size_t* count) {
42 if (!s || !count) return NULL;
43
44 /* First pass: count parts */
45 size_t n = 1;
46 for (const char* p = s; *p; p++) {
47 if (*p == delim) n++;
48 }
49
50 char** parts = malloc(n * sizeof(char*));
51 if (!parts) return NULL;
52
53 /* Second pass: extract parts */
54 size_t idx = 0;
55 const char* start = s;
56 for (const char* p = s; ; p++) {
57 if (*p == delim || *p == '\0') {
58 size_t len = (size_t)(p - start);
59 parts[idx] = malloc(len + 1);
60 if (!parts[idx]) {
61 str_split_free(parts, idx);
62 return NULL;
63 }
64 memcpy(parts[idx], start, len);
65 parts[idx][len] = '\0';
66 idx++;
67 if (*p == '\0') break;
68 start = p + 1;
69 }
70 }
71 *count = n;
72 return parts;
73}
74
75void str_split_free(char** parts, size_t count) {
76 if (parts) {
77 for (size_t i = 0; i < count; i++) {
78 free(parts[i]);
79 }
80 free(parts);
81 }
82}
83
84char* str_trim(char* s) {
85 if (!s) return NULL;
86 while (isspace((unsigned char)*s)) s++;
87 if (*s == '\0') return s;
88 char* end = s + strlen(s) - 1;
89 while (end > s && isspace((unsigned char)*end)) end--;
90 end[1] = '\0';
91 return s;
92}
tests/test_strutils.c
C
1#include <stdio.h>
2#include <string.h>
3#include "strutils.h"
4
5/* This file has its own file-scope static variable */
6static int tests_passed = 0;
7static int tests_total = 0;
8
9static void test(const char* name, int condition) {
10 tests_total++;
11 if (condition) {
12 tests_passed++;
13 printf(" PASS: %s\n", name);
14 } else {
15 printf(" FAIL: %s\n", name);
16 }
17}
18
19int main(void) {
20 printf("=== strutils tests ===\n\n");
21
22 /* Test str_count_char */
23 test("count 'l' in hello", str_count_char("hello", 'l') == 2);
24 test("count 'x' in hello", str_count_char("hello", 'x') == 0);
25
26 /* Test str_reverse */
27 char buf1[] = "hello";
28 str_reverse(buf1);
29 test("reverse hello", strcmp(buf1, "olleh") == 0);
30
31 /* Test str_is_palindrome */
32 test("racecar is palindrome", str_is_palindrome("racecar") == 1);
33 test("hello is not palindrome", str_is_palindrome("hello") == 0);
34
35 /* Test str_split */
36 size_t count = 0;
37 char** parts = str_split("a,b,c,d", ',', &count);
38 test("split count is 4", count == 4);
39 if (parts) {
40 test("parts[0] is a", strcmp(parts[0], "a") == 0);
41 test("parts[2] is c", strcmp(parts[2], "c") == 0);
42 str_split_free(parts, count);
43 }
44
45 printf("\n%d/%d tests passed\n", tests_passed, tests_total);
46 return (tests_passed == tests_total) ? 0 : 1;
47}
Common Mistakes

Multi-file C projects have several pitfalls. Missing include guards cause duplicate definitions. Forgetting extern creates multiple definitions. Including source files instead of headers breaks the compilation model.

mistake1.c
C
1/* MISTAKE 1: Defining a variable in a header without extern */
2/* bad_config.h */
3int global_count = 0; /* BAD — every file that includes this gets a copy */
4/* Result: "multiple definition of 'global_count'" linker error */
5
6/* Correct version */
7/* good_config.h */
8extern int global_count; /* Declaration — says "exists elsewhere" */
9/* good_config.c */
10int global_count = 0; /* Definition — exactly one copy */
mistakes2_3.c
C
1/* MISTAKE 2: Missing include guards */
2/* header.h (no guard) */
3typedef struct { int x; int y; } Point;
4typedef struct { int x; int y; } Point; /* Duplicate! */
5
6/* MISTAKE 3: Including .c files instead of .h files */
7/* main.c */
8#include "utils.c" /* BAD — causes duplicate symbols if utils.c is also compiled */
9#include "utils.h" /* GOOD — includes only declarations */
mistake4.c
C
1/* MISTAKE 4: Forgetting static on file-scope helpers */
2/* helpers.c */
3void internal_sort(int* arr, size_t n) { ... } /* Visible to linker */
4
5/* If another file also defines internal_sort → "multiple definition" */
6
7/* Fix: mark file-local functions as static */
8static void internal_sort(int* arr, size_t n) { ... } /* Hidden from linker */

best practice

Golden rules for multi-file projects: (1) Every header has include guards or #pragma once. (2) Variables in headers are always extern. (3) Functions used by only one file are static. (4) Never include .c files.
$Blueprint — Engineering Documentation·Section ID: C-MULTI-FILE·Revision: 1.0