This episode builds functions as modular units: definitions and prototypes, variable scope, passing parameters by value, and return values. You will also assemble multi-file programs with header files, and get to know recursion, inline functions, and function pointers.

A healthy program isn't written as one giant block; it's broken into small functions, each with a single responsibility. Episode 5 covers how to build and assemble those functions into a modular program.
Functions in C work on the passing by value concept: when a variable is sent to a function, what gets sent is a copy of its value, not the original variable. This concept often confuses beginners, but it's actually the key to safe design. To modify original data, you need pointers — a topic we'll dissect in episode 7.
You will also learn to separate code into header files and source files, build multi-file projects, and get to know recursion, inline functions, and function pointers that open the door to callbacks and generic programming.
A function consists of a return type, a name, a parameter list, and a function body. Functions that return no value are declared with the void type:
cat > fungsi.c <<'EOF'
#include <stdio.h>
int tambah(int a, int b);
int main(void) {
int hasil = tambah(3, 4);
printf("Hasil: %d\n", hasil);
return 0;
}
int tambah(int a, int b) {
return a + b;
}
EOF
gcc -Wall -Wextra fungsi.c -o fungsi && ./fungsiThe line int tambah(int a, int b); is a prototype: a declaration that tells the compiler about the function's signature before its definition appears. With a prototype, main can call tambah even though its definition comes later.
Every variable has a scope that determines where it can be accessed. Local variables live only within the block where they're declared, including function parameters. Global variables are declared outside all functions and can be accessed anywhere — but use them sparingly because they make it hard to track who changes the value.
When a function is called, the argument values are copied into the parameters. Changes inside the function don't affect the caller's original variables:
cat > byvalue.c <<'EOF'
#include <stdio.h>
void ubah(int x) {
x = 99;
}
int main(void) {
int nilai = 5;
ubah(nilai);
printf("nilai tetap %d\n", nilai);
return 0;
}
EOF
gcc -Wall -Wextra byvalue.c -o byvalue && ./byvalueEven though ubah sets x to 99, the variable nilai in main remains 5. This is passing by value behavior: only a copy is sent. To change the original nilai, the function must receive its address through a pointer, as we'll cover in episode 7.
A function returns its result via the return statement. This value can be used directly or stored in a variable. A simple rule: if a function produces a value used elsewhere, make it return that value rather than writing to a global variable.
For projects that grow large, separate the interface from the implementation. Header .h files contain the declarations other files are allowed to see, while source .c files contain the definitions. The standard layout of a module:
cat > math_utils.h <<'EOF'
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int kuadrat(int x);
#endif
EOF
cat > math_utils.c <<'EOF'
#include "math_utils.h"
int kuadrat(int x) {
return x * x;
}
EOF
cat > main.c <<'EOF'
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("Kuadrat 6 = %d\n", kuadrat(6));
return 0;
}
EOF
gcc -Wall -Wextra math_utils.c main.c -o app && ./appThe command gcc math_utils.c main.c -o app compiles both source files at once and links them. The MATH_UTILS_H symbol in #ifndef serves as an include guard that prevents the header from being processed twice — a topic we'll cover in detail in episode 10.
Each preprocessed .c file is called a translation unit. Interfaces between translation units are bridged by headers. Good discipline: every .c file includes its matching header, and every function used from outside must be declared in a header.
Recursive functions call themselves. Each call needs a base case that stops the recursion; otherwise a stack overflow occurs. The classic factorial example:
#include <stdio.h>
int faktorial(int n) {
if (n <= 1) {
return 1;
}
return n * faktorial(n - 1);
}
int main(void) {
printf("%d\n", faktorial(5));
return 0;
}The function faktorial(5) calls itself until it reaches n = 1 as the base case. Recursion is elegant for structures like trees, but for simple iteration a for loop is more efficient.
The inline keyword suggests the compiler insert the function body directly at the call site, avoiding call overhead. It's only a suggestion; the compiler makes the final decision.
A function pointer stores the address of a function, so a function can be chosen and called dynamically:
#include <stdio.h>
int tambah(int a, int b) { return a + b; }
int kali(int a, int b) { return a * b; }
int main(void) {
int (*operasi)(int, int) = kali;
printf("%d\n", operasi(4, 5));
return 0;
}The declaration int (*operasi)(int, int) creates a variable that points to a function taking two ints and returning an int. Function pointers are the foundation of callbacks, dispatch tables, and generic programming.
Tip
A function should do one thing and one thing only. If a function exceeds your editor screen, consider splitting it up. This makes testing and code review much easier.
Key takeaways:
In the next episode 6 we will discuss complex data types and arrays — one- and multi-dimensional arrays, strings as character arrays, struct, union, and enum as composite types, and typedef, aliases, and simple data structures.