Make 02: C and C++ – Compiling Real Projects
Summary: Use GNU Make to compile C and C++ programs. You will learn the compilation pipeline, standard Make variables, pattern rules, automatic header dependency tracking, and how to write a complete project Makefile that rebuilds only what has changed.
| Key | Value |
|---|---|
| OS | Ubuntu 24.04 LTS |
| Make version | GNU Make 4.3 |
| GCC version | 13 |
| G++ version | 13 |
| Working directory | ~/projects/makefile-tutorial |
| C standard flags | -Wall -Wextra -g |
| C++ standard flags | -Wall -Wextra -std=c++17 -g |
0. Prerequisites
- Completion of Tutorial 01 – Make Fundamentals (or equivalent knowledge of targets, prerequisites, recipes, variables, and automatic variables)
- GCC and G++ installed
Install the build tools if they are not present.
sudo apt update && sudo apt install -y build-essential
This installs gcc, g++, make, and related tools.
Verify the installation.
gcc --version
g++ --version
1. The Compilation Pipeline
Before writing a Makefile, understand what GCC does when it builds a C program. There are two distinct steps.
| Step | Command | Input | Output |
|---|---|---|---|
| Compile | gcc -c main.c -o main.o | .c source file | .o object file |
| Link | gcc main.o -o program | .o object files | executable |
Compiling translates source code into machine code but does not produce a runnable program. Linking combines one or more object files (and libraries) into an executable.
This separation is why Make is so useful for C projects. If you change one source file, Make only recompiles that one file and re-links. It does not recompile the files you did not touch.
2. Compiling a Single C File
Create a project directory with a simple C program.
mkdir -p ~/projects/makefile-tutorial/c-single
cd ~/projects/makefile-tutorial/c-single
Code language: JavaScript (javascript)
Create main.c.
#include <stdio.h>
int main(void) {
printf("Hello from C!\n");
return 0;
}
Code language: PHP (php)
Create a Makefile.
CC = gcc
CFLAGS = -Wall -Wextra -g
hello: main.o
$(CC) $^ -o $@
main.o: main.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f hello *.o
Code language: JavaScript (javascript)
Build and run.
make
./hello
gcc -Wall -Wextra -g -c main.c -o main.o
gcc main.o -o hello
Hello from C!
Code language: CSS (css)
| Variable | Purpose |
|---|---|
CC | The C compiler command |
CFLAGS | Flags passed during compilation (warnings, debug symbols, optimization) |
3. Multiple Source Files
Real projects have more than one file. Create a multi-file project.
mkdir -p ~/projects/makefile-tutorial/c-multi
cd ~/projects/makefile-tutorial/c-multi
Code language: JavaScript (javascript)
Create three files.
greet.h – the header declaring the function.
#ifndef GREET_H
#define GREET_H
void greet(const char *name);
#endif
Code language: PHP (php)
greet.c – the implementation.
#include <stdio.h>
#include "greet.h"
void greet(const char *name) {
printf("Hello, %s!\n", name);
}
Code language: PHP (php)
main.c – the entry point.
#include "greet.h"
int main(void) {
greet("Make");
return 0;
}
Code language: PHP (php)
Create a Makefile.
CC = gcc
CFLAGS = -Wall -Wextra -g
TARGET = hello
OBJS = main.o greet.o
$(TARGET): $(OBJS)
$(CC) $^ -o $@
main.o: main.c greet.h
$(CC) $(CFLAGS) -c $< -o $@
greet.o: greet.c greet.h
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS)
Code language: JavaScript (javascript)
Build and run.
make
./hello
gcc -Wall -Wextra -g -c main.c -o main.o
gcc -Wall -Wextra -g -c greet.c -o greet.o
gcc main.o greet.o -o hello
Hello, Make!
Code language: CSS (css)
Now change only greet.c (for example, change the message) and run make again. Only greet.o is recompiled and the program is re-linked. main.o is untouched because main.c did not change.
4. Standard Make Variables
C and C++ projects use a set of conventional variable names that Make and many tools recognize.
| Variable | Purpose | Typical value |
|---|---|---|
CC | C compiler | gcc |
CXX | C++ compiler | g++ |
CFLAGS | C compilation flags | -Wall -Wextra -g |
CXXFLAGS | C++ compilation flags | -Wall -Wextra -std=c++17 -g |
CPPFLAGS | Preprocessor flags (include paths) | -Iinclude |
LDFLAGS | Linker flags (library paths) | -Llib |
LDLIBS | Libraries to link | -lm -lpthread |
Note:
CPPFLAGSstands for C PreProcessor flags, not C++ flags. The C++ flags variable isCXXFLAGS.
These variables keep your Makefile flexible. A user can override them from the command line.
make CC=clang CFLAGS="-O2 -Wall"
Code language: JavaScript (javascript)
5. Pattern Rules
The Makefile in section 3 has a separate rule for every .o file. This gets tedious as the project grows. Pattern rules solve this by using the % wildcard.
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
Code language: JavaScript (javascript)
This single rule tells Make: “to build any .o file, compile the matching .c file.” The % matches the filename stem – main.o matches main.c, greet.o matches greet.c.
Rewrite the multi-file Makefile using a pattern rule.
CC = gcc
CFLAGS = -Wall -Wextra -g
TARGET = hello
SRCS = main.c greet.c
OBJS = $(SRCS:.c=.o)
$(TARGET): $(OBJS)
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS)
Code language: JavaScript (javascript)
$(SRCS:.c=.o) is a substitution reference – it takes the value of SRCS and replaces .c with .o, producing main.o greet.o.
Warning: This pattern rule does not track header dependencies. If you change
greet.h, Make will not know to recompile the files that include it. The next section fixes this.
6. Tracking Header Dependencies
When a C file includes a header, that header is a hidden prerequisite. If the header changes, the object file must be recompiled. Tracking this by hand is error-prone.
GCC can generate dependency information automatically using the -MMD and -MP flags.
gcc -MMD -MP -c main.c -o main.o
Code language: CSS (css)
This produces main.o as usual, plus a file called main.d containing the following.
main.o: main.c greet.h
Code language: CSS (css)
That is a Make rule. Include all .d files in your Makefile and Make will know about the header dependencies.
CC = gcc
CFLAGS = -Wall -Wextra -g -MMD -MP
TARGET = hello
SRCS = main.c greet.c
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)
$(TARGET): $(OBJS)
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
-include $(DEPS)
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS) $(DEPS)
Code language: JavaScript (javascript)
| Flag | Purpose |
|---|---|
-MMD | Generate .d dependency files alongside .o files (user headers only, not system headers) |
-MP | Add dummy targets for each header so make does not break if a header is deleted |
The -include directive (with the leading dash) tells Make to include the .d files if they exist and silently ignore them if they do not. On the first build the .d files do not exist yet, so the dash prevents an error.
Now if you change greet.h, Make automatically recompiles every file that includes it.
7. A Complete C Project
Here is a production-ready Makefile for a C project that combines everything from this tutorial.
# Compiler and flags
CC = gcc
CFLAGS = -Wall -Wextra -g -MMD -MP
LDFLAGS =
LDLIBS =
# Project files
TARGET = hello
SRCS = main.c greet.c
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)
# Default target
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
%.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
-include $(DEPS)
clean:
rm -f $(TARGET) $(OBJS) $(DEPS)
Code language: PHP (php)
This Makefile does the following.
- Compiles each
.cfile to a.ofile with warnings and debug symbols - Generates
.ddependency files so header changes trigger recompilation - Links all object files into the final executable
- Supports command-line overrides (
make CC=clang) - Cleans up all generated files with
make clean
8. Switching to C++
The same patterns work for C++. The key differences are the variable names and the compiler.
| C Variable | C++ Variable |
|---|---|
CC | CXX |
CFLAGS | CXXFLAGS |
.c files | .cpp files |
gcc | g++ |
Create a C++ version of the project.
mkdir -p ~/projects/makefile-tutorial/cpp-project
cd ~/projects/makefile-tutorial/cpp-project
Code language: JavaScript (javascript)
greet.h – the header.
#ifndef GREET_H
#define GREET_H
#include <string>
void greet(const std::string &name);
#endif
Code language: PHP (php)
greet.cpp – the implementation.
#include <iostream>
#include "greet.h"
void greet(const std::string &name) {
std::cout << "Hello, " << name << "!" << std::endl;
}
Code language: CSS (css)
main.cpp – the entry point.
#include "greet.h"
int main() {
greet("Make");
return 0;
}
Code language: PHP (php)
The Makefile mirrors the C version with C++ variables.
# Compiler and flags
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++17 -g -MMD -MP
LDFLAGS =
LDLIBS =
# Project files
TARGET = hello
SRCS = main.cpp greet.cpp
OBJS = $(SRCS:.cpp=.o)
DEPS = $(OBJS:.o=.d)
# Default target
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@
%.o: %.cpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c $< -o $@
-include $(DEPS)
clean:
rm -f $(TARGET) $(OBJS) $(DEPS)
Code language: PHP (php)
Build and run.
make
./hello
g++ -Wall -Wextra -std=c++17 -g -MMD -MP -c main.cpp -o main.o
g++ -Wall -Wextra -std=c++17 -g -MMD -MP -c greet.cpp -o greet.o
g++ main.o greet.o -o hello
Hello, Make!
Summary
You learned how to use Make for C and C++ projects.
- The compilation pipeline has two steps: compile (
.cto.o) and link (.oto executable) - Standard variables (
CC,CFLAGS,CXX,CXXFLAGS,LDFLAGS,LDLIBS) keep Makefiles portable and overridable - Pattern rules (
%.o: %.c) eliminate repetitive per-file rules - The
-MMD -MPflags generate dependency files so Make automatically tracks header changes -include $(DEPS)loads those dependency files without failing on the first build- Substitution references (
$(SRCS:.c=.o)) convert file lists between extensions
The same Makefile structure works for projects of any size – add source files to SRCS and Make handles the rest. The next tutorial shows how to use Make as a task runner for shell scripts, Python projects, and other non-compiled workflows.