C Project Structure
C project structure refers to the directory layout for organizing C language project source code, header files, build scripts, and resource files.
A clear and standardized project structure makes large C projects easy to maintain, facilitates multi-person collaboration, and simplifies the build and deployment processes.
Why is a standardized project structure needed?
When beginners write C code, they often dump all files into one directory, which causes serious problems as the project grows.
Duplicate header files, naming conflicts, slow compilation, and difficulty locating code — this kind of chaos ultimately makes the project hard to maintain.
A conventional directory structure helps you and your team quickly understand the overall project, and also allows automated build tools such as Make and CMake to work efficiently.
Standard directory structure
Below is a recommended directory structure for a typical C project, suitable for small and medium-sized projects.
Clear responsibilities and hierarchy for each directory:

Detailed explanation of core directories
The following explains the responsibilities and best practices of each directory one by one.
src/ — source code directory
Store all.cSource files: split into multiple files by module.
One .c file per module, with file names corresponding to functionality for quick location.
Entry functionmain()Usually placed in main.c; do not mix in business logic.
Example
#include "utils.h" /* Custom utility function header file */
#include "database.h" /* Database operation header file */
#include <stdio.h> /* Standard input/output */
int main(int argc, char *argv[]) {
/* argc: number of command-line arguments, argv: argument array */
printf("EXAMPLE C project startup\n");
init_database(); /* Initialize database connection */
print_version(); /* Print version information (from utils module) */
return 0; /* Return 0 to indicate normal program exit */
}
include/ — Header file directory
Store all.hHeader files are the interface definitions for communication between modules.
Header files contain only declarations (function prototypes, structs, macros, enums), not implementation code.
Example
#ifndef UTILS_H /* Header file guard macro, prevents duplicate inclusion */
#define UTILS_H
/* Project name and version number (macro constants, all uppercase naming) */
#define PROJECT_NAME "EXAMPLE"
#define VERSION_MAJOR 1
#define VERSION_MINOR 0
/* Struct: represents a 2D coordinate point */
typedef struct {
int x; /* X coordinate */
int y; /* Y coordinate */
} Point;
/* Function declaration: calculate the distance between two points */
double distance(Point a, Point b);
/* Function declaration: print version information */
void print_version(void);
#endif /* UTILS_H */
tests/ — test directory
Stores unit test code; typically one test file corresponds to one source module.
It is recommended to use testing frameworks such as CUnit and Check, or you can write simple assertion tests by hand.
build/ — build directory
Stores intermediate files (.o) generated by compilation and the final executable.
This directory is automatically generated and cleaned by the build system and is not included in version control (it should be added to .gitignore).
lib/ — third-party library directory
Stores third-party static libraries (.a) or dynamic libraries (.so / .dylib) that the project depends on.
It is more recommended to use package managers (such as vcpkg, Conan) to manage dependencies rather than manually copying library files.
doc/ — documentation directory
Stores project design documents, API documentation, changelogs, etc.
It is recommended to use Doxygen to automatically generate API documentation from source code comments.
Header file management
Header files are the key bridge for collaboration between modules in C projects; improper management can cause numerous compilation problems.
The following diagram shows the reference relationships among header files in a typical C project:

Each .c file has a corresponding .h file with the same name; the .h file serves as the module's public interface.
Header files must useHeader file guard macro(include guard) prevents duplicate inclusion.
Do not define global variables in header files. If multiple .c files include the same header file, duplicate definitions of global variables will cause link errors. The correct approach is to declare them with extern in the header file and define them in one .c file.
Build system
The build system is responsible for compiling source code into executables, managing compilation order, dependencies, and compiler options.
Makefile — classic build tool.
Makefile controls the compilation process by defining rules (target, dependencies, commands).
Suitable for small to medium projects; preinstalled on almost all Unix/Linux systems.
Example
# Makefile basic structure: target: dependencies\n\tcommand
# Compiler settings
CC = gcc # Specify the C compiler
CFLAGS = -Wall -Wextra -g # Compiler options: all warnings + debug information
INCLUDE = -I./include # Header file search path
TARGET = example_app # Final executable filename
# Source files and object files
SRCS = $(wildcard src/*.c) # Automatically collect all .c files under src/
OBJS = $(SRCS:.c=.o) # Derive corresponding .o file names
# Default target: build executable
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
@echo "Build successful: ./$(TARGET)"
# Compilation rules: .c → .o
%.o: %.c
$(CC) $(CFLAGS) $(INCLUDE) -c $< -o $@
# Clean build artifacts
.PHONY: clean
clean:
rm -f $(OBJS) $(TARGET)
@echo "Build files cleaned"
CMake — cross-platform build system
CMake describes the project structure via CMakeLists.txt and automatically generates build files for each platform.
Suitable for medium to large projects that require cross-platform support.
Example
# Minimum CMake version requirement
cmake_minimum_required(VERSION 3.10)
# Project name and language
project(example_app C)
# Set the C standard to C11
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Collect all source files
file(GLOB SOURCES "src/*.c")
# Define header file search paths
include_directories(include)
# Generate executable file
add_executable(${PROJECT_NAME} ${SOURCES})
# Optional: link third-party libraries
# target_link_libraries(${PROJECT_NAME} PRIVATE m)
CMake build steps:
$ mkdir build && cd build $ cmake .. $ make $ ./example_app EXAMPLE C 项目启动 版本: v1.0
Compilation process
Understanding the entire process of C source code from writing to running helps troubleshoot compilation and linking errors.
The following diagram shows the four stages from a .c file to an executable:
Step-by-step compilation commands
You can use GCC to execute it step by step and observe the output at each stage:
# 1. 预处理:展开所有宏和头文件 $ gcc -E src/main.c -I./include -o main.i # 2. 编译:将预处理结果转为汇编代码 $ gcc -S main.i -o main.s # 3. 汇编:将汇编代码转为目标文件 $ gcc -c main.s -o main.o # 4. 链接:将所有目标文件链接为可执行程序 $ gcc main.o utils.o -o example_app
Common Examples
The following is a complete small C project example, covering the entire process from directory creation to compilation and running.
Create project skeleton
$ mkdir -p example_project/{src,include,tests,build,lib,doc}
$ tree example_project/
example_project/
├── build/
├── doc/
├── include/
├── lib/
├── src/
└── tests/
Write module code
Example: math utility module
#include "math_utils.h" /* its own header file */
#include <math.h> /* standard math library, provides sqrt() */
/* Calculate the Euclidean distance between two points */
double distance(Point a, Point b) {
int dx = a.x - b.x; /* x direction difference */
int dy = a.y - b.y; /* y direction difference */
return sqrt(dx * dx + dy * dy); /* Pythagorean theorem */
}
/* Check if a number is prime */
int is_prime(int n) {
if (n < 2) return 0; /* Numbers less than 2 are not prime */
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return 0; /* Factor found, not a prime */
}
return 1; /* No factor found; it is a prime */
}
Example: corresponding header file
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
/* 2D point structure */
typedef struct {
int x;
int y;
} Point;
/* Calculate distance between two points */
double distance(Point a, Point b);
/* Determine whether n is prime, return 1 if yes, 0 if no */
int is_prime(int n);
#endif /* MATH_UTILS_H */
Compilation and Execution
$ cd example_project $ mkdir build && cd build $ cmake .. -- Configuring done -- Generating done $ make [100%] Built target example_app $ ./example_app EXAMPLE 项目运行成功! Point(0,0) 到 Point(3,4) 的距离 = 5.00 7 是质数: 是
Notes
Header guard macros must be unique. It is recommended to usePROJECT_NAME_MODULE_NAME_Hthe naming convention to avoid macro name conflicts between different projects.
Do not use #include "file.c" to reference source files. This will cause the same function to be compiled multiple times, resulting in duplicate definition errors. Always list all .c files in the compilation command, or use Makefile/CMake to manage them.
Summary of directory responsibilities:
| Directory | Responsibilities | Whether to include in version control |
|---|---|---|
| src/ | .c source files | Yes |
| include/ | .h header files, public interfaces | Yes |
| tests/ | Test Code | Yes |
| build/ | Build intermediate artifacts, executable files | No (add to .gitignore) |
| lib/ | Third-party library files | It depends |
| doc/ | Project Documentation | Yes |
A complete .gitignore example:
# 构建产物 build/ *.o *.out *.exe # IDE 配置 .vscode/ .idea/ # macOS .DS_Store
FAQ
Q: Why does the compiler report an 'undefined reference' error?
This is a link-stage error, indicating that the function was declared but its implementation was not found.
Check whether the corresponding .c file is included in the compilation command, or whether the library file is correctly linked.
Q: Should header files use <> or ""?
Standard library headers use <stdio.h>, and the compiler searches the system path.
Custom headers use "utils.h"; the compiler searches the current directory first, then the system path.
Q: What is the difference between static and dynamic libraries?
Static libraries (.a) are copied into the executable at link time; the program is self-contained but large in size.
Dynamic libraries (.so / .dylib) are loaded at runtime and can be shared by multiple programs; you need to ensure the corresponding version exists on the target system.
other extensions