Notifications

No notifications

/Phase 2

Functions in C

Reusable Blocks of Code 🧩

A function is a named block of code you can call from anywhere. Functions let you split a big problem into small, testable pieces.

int add(int a, int b) {
    return a + b;
}

int main(void) { int result = add(3, 5); // call the function printf("%d\n", result); // 8 }

Anatomy of a Function

return_type   name(parameter_type p1, parameter_type p2) {
    /* body */
    return some_value;
}

PartMeaning
return_typeWhat the function gives back (int, double, void, …)
nameWhat you call it by
parametersInputs the function receives
returnSends a value back to the caller
void"no return value" or "no parameters"

Three Common Shapes

/* Returns a value */
int square(int x) { return x * x; }

/* Returns nothing */ void greet(const char *name) { printf("Hello, %s!\n", name); }

/* Takes nothing, returns nothing */ void banner(void) { printf("======\n"); }

Function Prototypes (Forward Declarations)

C reads files top-to-bottom. If main calls add before add is defined, you must declare it at the top:

int add(int a, int b);   // prototype — just the signature

int main(void) { return add(2, 3); }

int add(int a, int b) { return a + b; } // definition later

Pass-by-Value (the only kind in C)

C always copies arguments. Modifying a parameter inside a function does not affect the caller's variable.

void inc(int x) { x++; }
int n = 5;
inc(n);
printf("%d\n", n);   // still 5

To modify the caller's variable, pass a pointer (covered in the Pointers topic).

On this page

Detailed Theory

Functions are the unit of structure in C. A C program is, fundamentally, a collection of functions — main being just one of them.

Why Functions?

Three reasons:

1. Reuse — write logic once, call it many times. 2. Abstraction — give a name to "what" instead of repeating "how". 3. Testability — small functions are easy to verify.

A program of 100 lines of main is hard to debug. A program of one 5-line main plus ten 10-line helpers is easy.

Declaration vs Definition

int add(int, int);             /* declaration (prototype) — promises it exists */
int add(int a, int b) { ... }  /* definition — actual body */

You can declare many times, but define exactly once. Prototypes go in header files (.h); definitions go in source files (.c).

> Pre-C99, omitting the prototype made the compiler assume int foo(). This is gone in C99+ — you must declare before use.

Why void in int main(void)?

Without void, the empty parentheses int main() historically meant "any number of arguments — I haven't told you yet". The modern, correct form is:

int main(void) { ... }

void explicitly says "no parameters".

Pass-by-Value — Always

C has no pass-by-reference. Every parameter is a copy:

void swap_wrong(int a, int b) {
    int t = a; a = b; b = t;   // swaps the copies — caller unchanged!
}

int x = 1, y = 2; swap_wrong(x, y); printf("%d %d", x, y); // 1 2 — nothing changed

To actually modify the caller's variables, pass addresses (pointers):

void swap_right(int *a, int *b) {
    int t = *a; *a = *b; *b = t;
}

int x = 1, y = 2; swap_right(&x, &y); printf("%d %d", x, y); // 2 1

The & takes the address; * dereferences it. We'll go deep on pointers next phase — but you've now seen why they exist.

Arrays Decay to Pointers

When you pass an array to a function, only the address of the first element is copied — not the whole array. That's why:

1. Modifying the array inside the function does affect the caller's array. 2. sizeof(arr) inside the function gives pointer size, not array size.

void zero(int a[], int n) {     /* a[] is really int * */
    for (int i = 0; i < n; i++) a[i] = 0;
}

This is why every array-taking function in C also takes the length as a separate parameter.

return — Hand a Value Back

int absolute(int x) {
    if (x < 0) return -x;
    return  x;
}

return immediately exits the function. You can have multiple returns — early-exits often make code clearer than deep nesting.

void functions can use return; (no value) to exit early.

Scope and Lifetime

Variables declared inside a function are local — they exist only while the function runs and are invisible elsewhere.

int counter(void) {
    int c = 0;       /* fresh each call — always starts at 0 */
    return ++c;
}

To preserve a value between calls, use static:

int counter(void) {
    static int c = 0;   /* initialised ONCE; survives between calls */
    return ++c;
}
counter();   // 1
counter();   // 2
counter();   // 3

static locals live for the entire program.

The Stack — How Calls Work

Every function call gets a stack frame: a chunk of memory holding parameters, locals, and the return address. When the function returns, its frame is popped.

main's frame
add's frame
← top — gets popped when add returns

That's why returning a pointer to a local variable is a bug — the memory is reclaimed:

int *bad(void) {
    int x = 42;
    return &x;     /* ❌ x dies when bad returns; pointer is dangling */
}

Recursion (preview)

A function calling itself is recursion:

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

Each call adds a stack frame. Beautiful for tree/divide-and-conquer problems but heavy on the stack — too deep and you get a stack overflow. Recursion gets its own dedicated topic in Phase 4.

Header File Pattern (real-world C)

/* mathx.h — declarations */
#ifndef MATHX_H
#define MATHX_H
int add(int, int);
int square(int);
#endif

/* mathx.c — definitions */ #include "mathx.h" int add(int a, int b) { return a + b; } int square(int x) { return x * x; }

/* main.c — uses them */ #include "mathx.h" int main(void) { return add(square(3), 4); }

The #ifndef … #define … #endif "include guard" prevents the same header from being parsed twice.

Functions Cheat-Sheet

NeedPattern
Return a valueint f(...) { return v; }
Don't returnvoid f(...) { }
No parametersint f(void)
Modify caller's varPass &var, take int *
Operate on arrayTake pointer + length
Persistent localstatic int n;

Functions are how you go from "writing code" to "designing programs."