Systems Architecture
4. Modular programming in C
Boni García
boni.garcia@uc3m.es
Telematic Engineering Department
School of Engineering
2024/2025
Table of contents
1. Introduction
2. The preprocessor
3. Modularity
4. Makefile
5. Static variables
6. Takeaways
Systems Architecture - 4. Modular programming in C 2
1. Introduction
So far, we have done C programs with all the logic inside the same
source file (e.g., my-program.c)
As C programs grow larger and larger, monolithic programs become
difficult to maintain, test, and debug
For this reason, it is often desirable to split the source code into
different files (called modules)
Modularity is important in C programming because it promotes code
readability, reusability, maintainability, and flexibility
Systems Architecture - 4. Modular programming in C 3
Table of contents
1. Introduction
2. The preprocessor
3. Modularity
4. Makefile
5. Static variables
6. Takeaways
Systems Architecture - 4. Modular programming in C 4
2. The preprocessor
The C preprocessor is a tool used automatically by the C compiler to
transform the program before actual compilation
Systems Architecture - 4. Modular programming in C 5
hello.c hello.i
1. Preprocessing
hello.s
2. Compilation 3. Assembly
hello.o
4. Linkage
hello
The C preprocessor
operates at the beginning
of the build process
2. The preprocessor
Preprocessor directives are lines included in the code of programs
preceded by a hash sign (#)
The preprocessor examines the code and resolves all these directives
before actual compilation
So far, we have seen a couple of preprocessor directives
Systems Architecture - 4. Modular programming in C 6
#include <standard_c_lib.h>
To use some C standard
library, such as stdio.h,
stdlib.h, etc.
#define MACRO value To declare a constant value
2. The preprocessor
The C preprocessor also allows conditional compilation through the
following directives:
There is a second directive for conditional compilation called
#ifndef, which is used typically for modular programming
Systems Architecture - 4. Modular programming in C 7
#ifdef MACRO
/* Code block 1 */
#else
/* Code block 2 */
#endif
If MACRO is defined, the first
code block is included for
compilation. Otherwise, the
second block is included
2. The preprocessor
Lets consider the following example:
Systems Architecture - 4. Modular programming in C 8
#include <stdio.h>
int main() {
printf("Hello world\n");
#ifdef DEBUG
fprintf(stderr, "This is a debug message\n");
#endif
return 0;
}
$ gcc debug_1.c && ./a.out
Hello world
By default, this message will
not be displayed, since
DEBUG is not defined in this
program
2. The preprocessor
GCC allows defining macros in the command line using the option D
This way, the previous example displays the debug message if we
define the macro DEBUG in the compilation command:
Systems Architecture - 4. Modular programming in C 9
$ gcc -Dname [options] [source files] [-o output file]
$ gcc -Dname=definition [options] [source files] [-o output file]
#include <stdio.h>
int main() {
printf("Hello world\n");
#ifdef DEBUG
fprintf(stderr, "This is a debug message\n");
#endif
return 0;
}
$ gcc debug_1.c && ./a.out
Hello world
$ gcc debug_1.c -DDEBUG && ./a.out
Hello world
This is a debug message
2. The preprocessor
In addition to constants, the directive #define also allows to create
macros with arguments
These macros work like regular functions in C. For instance:
Systems Architecture - 4. Modular programming in C 10
#define MACRO(arguments) expression
#include <stdio.h>
#ifdef DEBUG
#define debug(msg) fprintf(stderr, msg)
#else
#define debug(msg)
#endif
int main() {
printf("Hello world\n");
debug("This is a debug message\n");
return 0;
}
$ gcc debug_2.c && ./a.out
Hello world
$ gcc debug_2.c -DDEBUG && ./a.out
Hello world
This is a debug message
Table of contents
1. Introduction
2. The preprocessor
3. Modularity
4. Makefile
5. Static variables
6. Takeaways
Systems Architecture - 4. Modular programming in C 11
3. Modularity
For implementing modules in C, we need to separate the logic in two
different files:
Header files (.h), which contains functions declarations, global structures,
and macro definitions to be shared between several source files (.c)
Source files (.c) which contains the function definitions
Systems Architecture - 4. Modular programming in C 12
monolithic modular
program.c main.c
module1.h
module2.h
module1.c
module2.c
3. Modularity
We are going to study modularity through several examples. Consider
the following monolithic program that we want to convert in modular
Systems Architecture - 4. Modular programming in C 13
#include <stdio.h>
#define MAX_STR 80
typedef struct Person {
char name[MAX_STR];
int age;
} Person;
int sum_ages(Person a, Person b);
int main() {
Person alice = { "Alice", 25 };
Person bob = { "Bob", 32 };
printf("Alice and Bob has %d years together\n", sum_ages(alice, bob));
return 0;
}
int sum_ages(Person a, Person b) {
return a.age + b.age;
}
program.c
3. Modularity
We want to separate the declarations and macro definitions to a
header file (.h), and the functions definitions to a source file (.c)
Systems Architecture - 4. Modular programming in C 14
#include <stdio.h>
#define MAX_STR 80
typedef struct Person {
char name[MAX_STR];
int age;
} Person;
int sum_ages(Person a, Person b);
int main() {
Person alice = { "Alice", 25 };
Person bob = { "Bob", 32 };
printf("Alice and Bob has %d years together\n", sum_ages(alice, bob));
return 0;
}
int sum_ages(Person a, Person b) {
return a.age + b.age;
}
Macro definition
Structure declaration
Function declaration
Function definition
#ifndef PERSON_H
#define PERSON_H
#define MAX_STR 80
typedef struct Person {
char name[MAX_STR];
int age;
} Person;
int sum_ages(Person a, Person b);
#endif
#ifndef and #define are known
as header guards. Their primary
purpose is to prevent header files
from being included multiple times
3. Modularity
Systems Architecture - 4. Modular programming in C 15
#include <stdio.h>
#include "person.h"
int main() {
Person alice = { "Alice", 25 };
Person bob = { "Bob", 32 };
printf("Alice and Bob has %d years together\n",
sum_ages(alice, bob));
return 0;
}
#include "person.h"
int sum_ages(Person a, Person b) {
return a.age + b.age;
}
main.c
person.c
person.h
Notice that the directive #include
also allows to include custom header
files (when using " ")
3. Modularity
GCC allows compilating separately the modules, and then a linkage
the resulting object files into a single binary file
For instance, in the example before:
To simplify, and supposing that all modules of our program belong to
the same folder, we can compile and linkage all modules using a
single command
Systems Architecture - 4. Modular programming in C 16
gcc main.c -c
The flag -c compiles and assemble, but do not
link. In this example, it only produces main.o
gcc person.c -c It only produces person.o
gcc main.o person.o -o main It links main.o and person.o, producing the
executable program (called main in this example
gcc *.c -o main
It produces the executable program with a
single command. This command assumes all
source files (*.c) are in the same folder
3. Modularity
To see the importance of header guards, lets consider now another
example of a program composed of two modules:
Systems Architecture - 4. Modular programming in C 17
main.c
person.h
job.h
person.c
job.c
#ifndef PERSON_H
#define PERSON_H
#define MAX_STR 80
typedef struct Person {
char name[MAX_STR];
int age;
} Person;
int sum_ages(Person a, Person b);
#endif
3. Modularity
Systems Architecture - 4. Modular programming in C 18
#include <stdio.h>
#include "person.h"
#include "job.h"
int main() {
Person alice = { "Alice", 25 };
Person bob = { "Bob", 32 };
Job developer = { alice, "developer" };
Job tester = { bob, "tester" };
display_job(developer);
display_job(tester);
return 0;
}
#ifndef JOB_H
#define JOB_H
#include "person.h"
typedef struct Job {
Person person;
char role[MAX_STR];
} Job;
void display_job(Job job);
#endif
main.c
job.h
person.h
In file included from job.h:4,
from main.c:3:
person.h:4:16: error: redefinition of ‘struct Person’
4 | typedef struct Person {
| ^~~~~~
In file included from main.c:2:
person.h:4:16: note: originally defined here
4 | typedef struct Person {
| ^~~~~~
Without header
guards, we will get
compilation errors like
this:
3. Modularity
Systems Architecture - 4. Modular programming in C 19
When using global variables, we need to use the keyword extern in
the variables defined in other module:
#include <stdio.h>
#include "job.h"
extern Job company[];
void display_job(Job job) {
printf("%s is a %s\n", job.person.name, job.role);
}
void display_job_by_index(int i) {
display_job(company[i]);
}
#include <stdio.h>
#include "person.h"
#include "job.h"
Job company[MAX_JOBS];
int main() {
Person alice = { "Alice", 25 };
Person bob = { "Bob", 32 };
Job developer = { alice, "developer" };
Job tester = { bob, "tester" };
company[0] = developer;
company[1] = tester;
display_job_by_index(0);
display_job_by_index(1);
return 0;
}
main.c
job.c
3. Modularity
Common bad practices in modular programming in C are:
Include global variables in headers files
Include functions definitions in headers file:
Systems Architecture - 4. Modular programming in C 20
person.h
#ifndef JOB_H
#define JOB_H
typedef struct Job {
Person person;
char role[MAX_STR];
} Job;
Job company[MAX_JOBS];
#endif
This might lead to
multiple definition errors
void display_job(Job job) {
printf("%s is a %s\n", job.person.name, job.role);
}
void display_job_by_index(int i) {
display_job(company[i]);
}
job.h
Table of contents
1. Introduction
2. The preprocessor
3. Modularity
4. Makefile
5. Static variables
6. Takeaways
Systems Architecture - 4. Modular programming in C 21
4. Makefile
Systems Architecture - 4. Modular programming in C 22
The make tool allows managing and maintaining computer programs
consisting in several component files
The make tool reads the instruction defined in a file called Makefile
(also known as descriptor file)
The Makefile file is composed by a sets a set of rules to determine
which parts of a program need to be compiled, how it is executed, or
how to clean the intermediate file (e.g. object files)
https://www.gnu.org/software/make/
4. Makefile
Systems Architecture - 4. Modular programming in C 23
A Makefile is made up of different sections, each one containing:
Target: Normally, an executable or object file
Dependencies: Source code or other targets
Rules: Set of commands needed to make the target
Also, it is possible to define variables in a Makefile:
# Comment
target: dependency
command_1
command_2
...
command_N
Important: every rule line
begins with a tab, not spaces
VAR_NAME=value
4. Makefile
Systems Architecture - 4. Modular programming in C 24
For example (module 1):
CFLAGS=-Wall
compile:
gcc $(CFLAGS) main.c -c
gcc $(CFLAGS) person.c -c
gcc $(CFLAGS) main.o person.o -o main
clean:
rm -f *.o
rm -f main
run: compile
./main
$ make
gcc -Wall main.c -c
gcc -Wall person.c -c
gcc -Wall main.o person.o -o main
$ make run
gcc -Wall main.c -c
gcc -Wall person.c -c
gcc -Wall main.o person.o -o main
./main
Alice and Bob has 57 years together
$ make clean
rm -f *.o
rm -f main
4. Makefile
Systems Architecture - 4. Modular programming in C 25
Another example (module 2):
$ make
gcc -Wall *.c -o main
$ make run
gcc -Wall *.c -o main
./main
Alice is a developer
Bob is a tester
$ make clean
rm -f main
CFLAGS=-Wall
compile:
gcc $(CFLAGS) *.c -o main
clean:
rm -f main
run: compile
./main
Table of contents
1. Introduction
2. The preprocessor
3. Modularity
4. Makefile
5. Static variables
6. Takeaways
Systems Architecture - 4. Modular programming in C 26
5. Static variables
Static variables are defined using the keyword static
These variables are initialized only once
Therefore, the compiler persists with the variable till the end of the program
Systems Architecture - 4. Modular programming in C 27
#include <stdio.h>
void my_function() {
int regular_int = 0;
static int static_int = 0;
regular_int++;
static_int++;
printf("regular_int = %d, static_int = %d\n", regular_int, static_int);
}
int main() {
for (int i = 0; i < 10; i++) {
my_function();
}
}
regular_int = 1, static_int = 1
regular_int = 1, static_int = 2
regular_int = 1, static_int = 3
regular_int = 1, static_int = 4
regular_int = 1, static_int = 5
regular_int = 1, static_int = 6
regular_int = 1, static_int = 7
regular_int = 1, static_int = 8
regular_int = 1, static_int = 9
regular_int = 1, static_int = 10
5. Static variables
We can also use the static keyword for implementing encapsulation
in module (i.e., access restriction):
Static global variables are not visible outside of the file they are defined
in
Static functions are not visible outside of the C file they are defined in
Systems Architecture - 4. Modular programming in C 28
#include <stdio.h>
#include "person.h"
#include "job.h"
static Job company[MAX_JOBS];
int main() {
// ...
return 0;
}
For instance, this variable
can only be used in this
file (even if other files try
to access with extern)
Table of contents
1. Introduction
2. The preprocessor
3. Modularity
4. Static variables
5. Takeaways
Systems Architecture - 4. Modular programming in C 29
6. Takeaways
The C preprocessor is a used automatically by the C compiler to expand
macros (e.g. #include, #define) or conditional compiling (e.g. #ifded,
#ifndef)
GCC allows defining macros in the command line using the option -D (e.g., for
debugging)
For modular programs in C, we need to separate the logic into headers (.h)
and source (.c) files
Header files (.h) will contain functions declarations, global structures, and
macro definitions, while source files (.c) will contain the function definitions
The make tool reads the instructions defined in a file called Makefile (also
known as descriptor file) to compile, execute or clean C programs
Static variables (defined with the keyword static) in C are initialized only
once
Systems Architecture - 4. Modular programming in C 30