C has no module system or package manager built into the language. Instead, you split a program into header files (.h) holding declarations and source files (.c) holding implementations, compile each source file separately, and let the linker stitch the results together. This chapter covers that split, separate compilation, static and shared libraries, and the basics of Make and CMake.
Splitting headers and sources
A header contains only what other files need to know: prototypes, type definitions, and constants. Function bodies live in the source file. An include guard keeps the header harmless if it ends up included twice in the same translation unit.
// mathutil.h
#ifndef MATHUTIL_H
#define MATHUTIL_H
#include <stddef.h>
typedef struct {
double min, max, mean;
} Stats;
int clamp(int value, int lo, int hi);
Stats compute_stats(const double *values, size_t count);
#endif // MATHUTIL_H
// mathutil.c
#include "mathutil.h"
static double sum(const double *values, size_t count) { // private to this file
double total = 0.0;
for (size_t i = 0; i < count; i++) total += values[i];
return total;
}
int clamp(int value, int lo, int hi) {
if (value < lo) return lo;
if (value > hi) return hi;
return value;
}
Stats compute_stats(const double *values, size_t count) {
Stats s = { values[0], values[0], 0.0 }; // assumes count >= 1
for (size_t i = 1; i < count; i++) {
if (values[i] < s.min) s.min = values[i];
if (values[i] > s.max) s.max = values[i];
}
s.mean = sum(values, count) / (double)count;
return s;
}
// main.c
#include <stdio.h>
#include "mathutil.h"
int main(void) {
double data[] = {3.0, 9.5, 1.5, 6.0};
Stats s = compute_stats(data, sizeof data / sizeof data[0]);
printf("min %.1f, max %.1f, mean %.2f\n", s.min, s.max, s.mean);
printf("clamp: %d\n", clamp(120, 0, 100));
return 0;
}
A few rules of thumb:
- Use
#include <...> for system headers and #include "..." for your own. - Marking a function
static gives it internal linkage, so nothing outside the file can see it. Hide anything that is not part of the public interface. - To share a global variable, declare it in the header as
extern int counter; and define it as int counter = 0; in exactly one source file. #pragma once is not standard C, but every major compiler supports it as an alternative to include guards.
Separate compilation and linking
Compile each source file to an object file with -c, then link the objects. When you edit one file, only that file needs recompiling.
gcc -std=c17 -Wall -Wextra -c mathutil.c -o mathutil.o
gcc -std=c17 -Wall -Wextra -c main.c -o main.o
gcc main.o mathutil.o -o stats
./stats
Leave mathutil.o out of the link step and you get an error like "undefined reference to compute_stats". Compilation succeeded because the header declared the function, but the linker could not find a definition. The opposite mistake, defining the same function in two source files, produces a "multiple definition" error.
Static and shared libraries
Bundling object files into a library makes them easy to reuse across programs.
# static library
ar rcs libmathutil.a mathutil.o
gcc main.o -L. -lmathutil -o stats_static
# shared library (Linux)
gcc -fPIC -c mathutil.c -o mathutil.pic.o
gcc -shared mathutil.pic.o -o libmathutil.so
gcc main.o -L. -lmathutil -Wl,-rpath,'$ORIGIN' -o stats_shared
-L adds a directory to the library search path, and -lmathutil means libmathutil.a or libmathutil.so. The GNU linker resolves symbols left to right, so list libraries after the object files that use them. A shared library also has to be found when the program runs, so either embed an rpath as above or install it to a standard library directory.
Make and CMake
Once a project has more than a handful of files, let a build tool run the commands. Make describes rules as "target: prerequisites" followed by a recipe, and rebuilds only what is out of date. Recipe lines must be indented with a tab character, not spaces.
CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g
OBJS = main.o mathutil.o
stats: $(OBJS)
$(CC) $(OBJS) -o $@
%.o: %.c mathutil.h
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f stats $(OBJS)
.PHONY: clean
$@ expands to the target and $< to the first prerequisite. Run make to build and make clean to remove the outputs.
CMake works one level higher: it generates build files for Make, Ninja, Visual Studio, and others, and it hides compiler-specific flags. That makes it the common choice for projects that build on several platforms.
cmake_minimum_required(VERSION 3.16)
project(stats C)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_library(mathutil STATIC mathutil.c)
target_include_directories(mathutil PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(stats main.c)
target_link_libraries(stats PRIVATE mathutil)
cmake -S . -B build
cmake --build build
./build/stats
Change STATIC to SHARED in add_library to produce a shared library instead. All outputs land in the separate build directory, which keeps the source tree clean.
Summary
- Declarations go in headers, definitions in sources, and every header gets an include guard.
- Use
static to keep non-public functions and globals private to their file. - Compile each file with
-c and link at the end; "undefined reference" is a link-time error. - Static libraries are copied into the executable; shared libraries are loaded at run time.
- Make suits small projects; CMake is the usual choice for cross-platform builds.
0 comments
Sign in · Sign in to leave a comment.