Modular Programming #
Splitting into Multiple files.
Using Makefiles.
Pre-Class Work #
Read, understand, implement and test functions in the code: https://onlinegdb.com/fOgZ6HXyKw
You may need to download this code locally (copy paste to local c file).
Make sure your implementation pass all the test cases (option 15)
Installing GDB and Make #
See directions here
π§© Why Modularize Code? #
Problem:
- Large C programs become hard to manage when everything is in one
.cfile. - Difficult to debug, maintain, and reuse.
Solution:
- Split code into multiple files, each handling a clear responsibility.
- Use headers (
.h) for declarations and source files (.c) for definitions.
Benefits:
- Easier to understand and modify.
- Enables team collaboration.
- Promotes reusability and faster compilation.
π― Goal #
Weβll start with a small C program written in a single file, and gradually convert it into a multi-file modular program built using a Makefile.
At each step, weβll discuss the benefits and improvements introduced.
Step 0: Single File (Unmodularized) #
Letβs start with a simple C program that performs arithmetic operations.
calculator.c
#
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int divide(int a, int b) {
if (b == 0) {
printf("Error: Division by zero!\n");
return 0;
}
return a / b;
}
int main() {
int x = 10, y = 5;
printf("Add: %d\n", add(x, y));
printf("Subtract: %d\n", subtract(x, y));
printf("Multiply: %d\n", multiply(x, y));
printf("Divide: %d\n", divide(x, y));
return 0;
}
Compile with:
gcc calculator.c -o calculator
./calculator
β
Works fine for small projects.
β Problems appear as the codebase grows:
- Hard to locate specific functions.
- Difficult to debug.
- Changes in one part require recompiling everything.
- Not reusable in other projects.
Step 1: Split Logic into Separate Files #
We move all arithmetic logic into a new source file.
math_utils.c
#
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int multiply(int a, int b) {
return a * b;
}
int divide(int a, int b) {
if (b == 0) {
printf("Error: Division by zero!\n");
return 0;
}
return a / b;
}
main.c
#
#include <stdio.h>
// Declare functions defined elsewhere
int add(int, int);
int subtract(int, int);
int multiply(int, int);
int divide(int, int);
int main() {
int x = 10, y = 5;
printf("Add: %d\n", add(x, y));
printf("Subtract: %d\n", subtract(x, y));
printf("Multiply: %d\n", multiply(x, y));
printf("Divide: %d\n", divide(x, y));
return 0;
}
Compile both together:
gcc main.c math_utils.c -o calculator
β Advantages now:
- Arithmetic logic is separated from main logic.
- You can reuse
math_utils.cin another project. - Easier debugging and organization.
β Still not ideal:
- Manual declarations in
main.care error-prone. - If function signatures change, we must update multiple files.
Step 2: Introduce a Header File #
We define all function prototypes in a .h file.
math_utils.h
#
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
#endif
Update main.c
#
#include <stdio.h>
#include "math_utils.h"
int main() {
int x = 10, y = 5;
printf("Add: %d\n", add(x, y));
printf("Subtract: %d\n", subtract(x, y));
printf("Multiply: %d\n", multiply(x, y));
printf("Divide: %d\n", divide(x, y));
return 0;
}
Update math_utils.c
#
#include <stdio.h>
#include "math_utils.h"
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) {
if (b == 0) {
printf("Error: Division by zero!\n");
return 0;
}
return a / b;
}
Compile again:
gcc main.c math_utils.c -o calculator
β Advantages:
main.cdoesnβt need manual declarations.- Header file acts as a public interface for
math_utils.c. - Code is cleaner and less error-prone.
- Future modules can include
"math_utils.h"easily.
Step 3: Organize into Folders #
Letβs move to a standard directory structure:
calculator/
βββ include/
β βββ math_utils.h
βββ src/
β βββ main.c
β βββ math_utils.c
Now compile with include path:
gcc -I./include src/main.c src/math_utils.c -o calculator
β Advantage:
- Follows real-world conventions.
- Keeps headers, sources, and binaries neatly separated.
- Scales well for large projects.
π§° Compiling Multi-File Projects Manually #
gcc -c src/math_utils.c -o src/math_utils.o
gcc -c src/main.c -o src/main.o
gcc src/main.o src/math_utils.o -o program
-cβ compile to object file (no linking yet).- The final command links object files into an executable.
ποΈ Why Use a Makefile? #
Without a Makefile:
- You must retype all commands each time.
- Every file recompiles even if unchanged.
With a Makefile:
- Automates the build process.
- Rebuilds only changed files.
- Ensures consistent compiler flags.
Step 4: Add a Makefile #
We automate the build.
Makefile
#
See comments for detailed explanation.
# ============================================================
# Makefile for a simple calculator project
# ============================================================
# This Makefile automates the process of compiling and linking
# multiple C source files into a single executable.
# ============================================================
# The compiler to use
CC = gcc
# Compiler flags:
# -Wall : enables all common warnings
# -I./include : tells the compiler to look for header files in the 'include' folder
# -g : includes debugging information (useful for gdb)
CFLAGS = -Wall -I./include -g
# List of all source files (.c files)
# These are located in the 'src' directory
SRCS = src/main.c src/math_utils.c
# Object files (.o) will be generated for each source file
# The substitution rule below replaces every ".c" with ".o"
OBJS = $(SRCS:.c=.o)
# The final name of the executable program
TARGET = calculator
# ============================================================
# Default target: 'all'
# ============================================================
# When you type 'make' with no arguments, this target runs.
# It depends on $(TARGET), so the rules for $(TARGET) will run.
all: $(TARGET)
# ============================================================
# Linking stage
# ============================================================
# To build the final executable $(TARGET), we need all object files.
# $@ refers to the target name (i.e., 'calculator')
# $^ refers to all the prerequisites (i.e., all .o files)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
# ============================================================
# Compilation stage (pattern rule)
# ============================================================
# This rule describes how to make any .o file from its corresponding .c file.
# $< refers to the first prerequisite (the .c file)
# $@ refers to the target name (the .o file)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# ============================================================
# Clean target
# ============================================================
# Typing 'make clean' removes all generated files.
# -f flag ignores errors if files don't exist.
clean:
rm -f $(OBJS) $(TARGET)
# ============================================================
# End of Makefile
# ============================================================
# Usage:
# make β builds the project
# make clean β removes compiled files
# ============================================================
Now just run:
make
./calculator
make clean
β Advantages:
- No need to manually type long gcc commands.
- Automatically rebuilds only changed files.
- Keeps the build consistent and repeatable.
make cleanquickly resets your workspace.
π― Step 5: Scaling Up β Adding More Modules #
Now that our calculator is modular and buildable with a Makefile, letβs extend it to perform:
- Input/Output (handled by
io_utils) - Statistics (mean, variance) β handled by
stats
Weβll see how to integrate these new modules seamlessly into the existing structure.
π§± Updated Directory Structure #
calculator/
βββ include/
β βββ math_utils.h
β βββ io_utils.h
β βββ stats.h
βββ src/
β βββ main.c
β βββ math_utils.c
β βββ io_utils.c
β βββ stats.c
βββ Makefile
π₯οΈ io_utils Module β Handling Input and Output #
include/io_utils.h
#
#ifndef IO_UTILS_H
#define IO_UTILS_H
void get_two_numbers(int *a, int *b);
void print_result(const char *operation, int result);
#endif
src/io_utils.c
#
#include <stdio.h>
#include "io_utils.h"
void get_two_numbers(int *a, int *b) {
printf("Enter two integers: ");
scanf("%d %d", a, b);
}
void print_result(const char *operation, int result) {
printf("%s result = %d\n", operation, result);
}
π stats Module β Basic Statistical Calculations #
include/stats.h
#
#ifndef STATS_H
#define STATS_H
double mean(int arr[], int n);
double variance(int arr[], int n);
#endif
src/stats.c
#
#include <stdio.h>
#include "stats.h"
double mean(int arr[], int n) {
double sum = 0;
for (int i = 0; i < n; i++)
sum += arr[i];
return sum / n;
}
double variance(int arr[], int n) {
double m = mean(arr, n);
double sum_sq = 0;
for (int i = 0; i < n; i++)
sum_sq += (arr[i] - m) * (arr[i] - m);
return sum_sq / n;
}
π§ Updated main.c
#
src/main.c
#
#include <stdio.h>
#include "math_utils.h"
#include "io_utils.h"
#include "stats.h"
int main() {
int a, b;
get_two_numbers(&a, &b);
print_result("Addition", add(a, b));
print_result("Subtraction", subtract(a, b));
print_result("Multiplication", multiply(a, b));
print_result("Division", divide(a, b));
int data[5] = {a, b, a + b, a - b, a * b};
printf("\nStats on sample data: ");
printf("\nMean = %.2f", mean(data, 5));
printf("\nVariance = %.2f\n", variance(data, 5));
return 0;
}
βοΈ Updated Makefile #
Makefile
#
CC = gcc
CFLAGS = -Wall -I./include -g
SRCS = src/main.c src/math_utils.c src/io_utils.c src/stats.c
OBJS = $(SRCS:.c=.o)
TARGET = calculator
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
π Build and Run #
make
./calculator
Sample Output:
Enter two integers: 10 5
Addition result = 15
Subtraction result = 5
Multiplication result = 50
Division result = 2
Stats on sample data:
Mean = 17.40
Variance = 254.24
π§© Advantages of Scaling Up #
| Challenge | Modular Solution |
|---|---|
| More functionality | Add new .c and .h files easily |
| Maintenance | Modify one module without breaking others |
| Reuse | io_utils and stats can be used in other projects |
| Build complexity | Makefile handles it automatically |
| Teamwork | Different modules can be owned by different developers |
π§ Recap: Incremental Journey #
| Step | Change | Key Takeaway |
|---|---|---|
| 0 | Single File | Simple but messy |
| 1 | Split into .c files | Logical separation |
| 2 | Added .h files | Centralized declarations |
| 3 | Folder structure | Organized and scalable |
| 4 | Added Makefile | Automated builds |
| 5 | Multiple modules | Extend functionality easily |
| 6 | Added I/O + Stats | Real modular project ready for teamwork |
β Final Thoughts #
Modular programming is not just about organization β itβs about:
- Reusability
- Maintainability
- Team collaboration
- Faster builds and debugging
With a Makefile and clear module boundaries, your C projects can grow without becoming chaotic.
π§ Example in Practice #
Project: Mini Social Network
mini_social_network/
βββ include/
β βββ network.h
β βββ friendships.h
βββ src/
β βββ main.c
β βββ network.c
β βββ friendships.c
βββ Makefile
Each file handles one major concern:
- network.c β Core data structures and file I/O
- friendships.c β Relationship algorithms
- main.c β CLI interface
- Makefile β Automates the build