Introduction
In this lesson, you’ll move from writing individual C files to organizing a complete, maintainable project. You’ll learn how to structure folders, name files clearly, use a Makefile for automated builds, track your work with version control, and document your project so that others (and future you) can understand and extend it easily.
Designing a Folder Structure for a C Project
A clear folder layout keeps growing projects manageable. For a small-to-medium C project, a common structure is:
my_c_project/
├── src/ # All .c source files
├── include/ # All .h header files
├── build/ # Compiled object files, binaries (often auto-created)
├── tests/ # Test source files and data
├── docs/ # Documentation
├── Makefile # Build automation
└── README.md # Project overview and usage
Why Separate src and include?
src/collects implementation files (.c), which contain function definitions.include/collects interface files (.h), which expose function prototypes, type definitions, and macros.
This separation makes it easy to:
- See what your public interfaces are.
- Reuse headers across multiple executables or libraries.
- Set up the compiler’s include path cleanly (e.g.,
-Iinclude).
Example: Minimal Project Layout
my_c_project/
├── src/
│ ├── main.c
│ ├── math_utils.c
│ └── io_utils.c
├── include/
│ ├── math_utils.h
│ └── io_utils.h
├── Makefile
└── README.md
Each .c file has a corresponding header if it exposes functionality to other parts of the program.
File Naming Conventions
Consistent naming makes it easier to find and understand code.
General Guidelines
- Use lowercase with underscores:
snake_case(e.g.,math_utils.c). - Match header and source names:
math_utils.c↔math_utils.h. - Use descriptive names related to purpose rather than implementation details.
Examples:
main.c– program entry point only.math_utils.c/math_utils.h– math helper functions.file_manager.c/file_manager.h– file-related functionality.
Avoid vague names like misc.c or temp.c in real projects; they accumulate unrelated code and become hard to maintain.
Creating a Makefile for Automated Builds
Manual compilation works for tiny programs, but it quickly becomes painful as files multiply. A Makefile automates compilation and links only what changed.
Example Project Files
Assume this structure:
my_c_project/
├── src/
│ ├── main.c
│ ├── math_utils.c
│ └── io_utils.c
├── include/
│ ├── math_utils.h
│ └── io_utils.h
└── Makefile
Sample Makefile
# Simple Makefile for a small C project
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -Iinclude
SRC_DIR = src
BUILD_DIR = build
SRCS = $(SRC_DIR)/main.c \
$(SRC_DIR)/math_utils.c \
$(SRC_DIR)/io_utils.c
OBJS = $(SRCS:$(SRC_DIR)/%.c=$(BUILD_DIR)/%.o)
TARGET = $(BUILD_DIR)/my_c_project
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -o $@ $^
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -rf $(BUILD_DIR)
Explanation and Output
# Output (terminal commands and results)
$ make
# ...object files compiled into build/
# ...executable created as build/my_c_project
$ ./build/my_c_project
# Program runs
$ make clean
# build directory removed
SRCSlists your C source files.OBJSautomatically maps each.cinsrc/to a.oinbuild/.makebuilds the project;make cleanremoves build artifacts.
You can extend this Makefile later to add tests, debug builds, or different targets.
Using Version Control (Git) for Project Management
Version control lets you track changes, experiment safely, and collaborate.
Initializing a Git Repository
From your project root (my_c_project):
# Initialize repository and create first commit
$ git init
$ git add .
$ git commit -m "Initial project structure with Makefile"
Ignoring Build Artifacts
You usually don’t want compiled files and build directories in your repository. Add a .gitignore file:
# .gitignore
build/
*.o
*.exe
*.log
Then:
$ git status
# Should show only relevant source, headers, docs, etc.
Working with Branches (Conceptually)
For new features or experiments, you typically create a branch:
$ git checkout -b feature/new-math-functions
# edit src/math_utils.c, include/math_utils.h, etc.
$ git add src/math_utils.c include/math_utils.h
$ git commit -m "Add new math functions"
Branches help isolate work and keep the main branch stable.
Documenting Project Structure and Components
Good documentation explains what your project does and how it’s organized.
Writing a Minimal README.md
Create a README.md in the project root:
# My C Project
## Overview
A small C project demonstrating modular code structure, automated builds with Makefile,
and basic utilities for input/output and math operations.
## Project Structure
- `src/` – C source files (`main.c`, `math_utils.c`, `io_utils.c`)
- `include/` – Header files declaring public interfaces
- `build/` – Compiled object files and the final executable (created by `make`)
- `Makefile` – Build rules for compiling the project
## Building and Running
```bash
make # build the project
./build/my_c_project # run the executable
make clean # remove build artifacts
Components
math_utils– arithmetic helper functionsio_utils– input/output helper functions
This gives new developers (or future you) a quick orientation.
### Inline Documentation with Comments
Use comments in your headers to explain the purpose of each module and function. Example `include/math_utils.h`:
```c
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
/* math_utils.h
* Utility functions for common arithmetic operations
*/
int add(int a, int b);
int subtract(int a, int b);
#endif
This complements your external docs and helps tooling (and humans) understand the code.
Small Practical Example: Using the Project Layout
src/main.c
#include <stdio.h>
#include "math_utils.h"
int main(void) {
int x = 5;
int y = 3;
int sum = add(x, y);
int diff = subtract(x, y);
printf("%d + %d = %d\n", x, y, sum);
printf("%d - %d = %d\n", x, y, diff);
return 0;
}
include/math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int subtract(int a, int b);
#endif
src/math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
Explanation and Output
# Output (after running the built program)
5 + 3 = 8
5 - 3 = 2
- The
main.cfile uses functions declared inmath_utils.hand defined inmath_utils.c. - The Makefile compiles all
.cfiles and links them into a single executable. - The folder structure keeps each responsibility in a clear, predictable place.
Summary / Key Takeaways
In this lesson Setting Up Project Structure, you have learned:
- Organized Folder Layout: How to arrange C projects into
src/,include/,build/,tests/, anddocs/to keep code and artifacts cleanly separated. - Consistent Naming Conventions: Why matching
.cand.hfilenames and using descriptive, snake_case names improves readability and maintenance. - Automated Builds with Makefile: How to create a Makefile that compiles multiple source files, manages object files in a
build/directory, and provides acleantarget. - Version Control Integration: How to initialize a Git repository, use
.gitignoreto exclude build artifacts, and commit changes in a structured way. - Project Documentation Basics: How to write a
README.mdthat describes project purpose, structure, and build/run instructions. - Module-Level Documentation: How to document headers and components with comments so that interfaces and responsibilities are clear.
- Practical Project Example: How all these elements (folders, naming, Makefile, and documentation) come together in a small but realistic multi-file C project.