Functions in C Programming: Complete Guide with Worked Examples

Learn how C functions behave in memory, why ordinary swaps fail, how arrays decay, and how recursive calls unwind through clear code traces.

KnowledgeGate Team

Exam prep & CS education

Updated 19 Aug 20266 min read

Function syntax takes five minutes to memorise, but output prediction, recursion traces, and failed swaps still cost marks. Functions are where C's call-by-value rule, call stack, and storage classes collide. That collision is what the exam charges you for: swapVal leaves its caller untouched, sizeof on an array parameter reports 8 bytes instead of 20, and fib(5) burns 15 calls to return a single digit.

What a function is: declaration, definition, and call

Every C function involves three parts. A declaration, or prototype, gives the signature before first use. A definition supplies the body. A call transfers control to it and can receive the result.

In int add(int a, int b) { return a + b; }, int is the return type, add the name, (int a, int b) the parameter list, the braces the body, and return the returned value. A void return type sends no value. (void) means no arguments.

int add(int, int);

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

int r = add(3, 4);

The call computes 3 + 4 = 7, so r = 7. The prototype lets the compiler check the call. Older compilers could make implicit assumptions without one and mishandle a floating-point argument. A missing prototype is a defect, not a style nit.

Parameter passing: call by value vs call by "reference"

C is always pass-by-value. Each argument is copied. "Call by reference" in C means copying a pointer that still leads to the caller's variable.

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

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

int x = 5, y = 9;
swapVal(x, y);
swapRef(&x, &y);

After swapVal(x, y), x = 5 and y = 9. Its private copies changed from a = 5, b = 9 to a = 9, b = 5, then disappeared. Starting again from x = 5, y = 9, swapRef(&x, &y) dereferences the addresses and writes into main. The result is x = 9, y = 5.

If a function must change the caller's variable, pass its address, not merely its value.

Memory diagram contrasting swapVal, which leaves x=5 and y=9 unchanged, with swapRef, whose pointers swap them to x=9 and y=5.

Passing arrays and strings: the pointer-decay trap

An array passed to a function decays to a pointer to its first element. The function cannot recover its length, so pass that separately.

int sum(int arr[], int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}

int a[5] = {2, 4, 6, 8, 10};
int total = sum(a, 5);

The accumulator moves through 0 + 2 = 2, 2 + 4 = 6, 6 + 6 = 12, 12 + 8 = 20, and 20 + 10 = 30. Therefore, sum(a, 5) = 30.

Inside sum, sizeof(arr) gives the pointer size, 8 bytes on a 64-bit machine in this example, not the 20 bytes occupied by five 4-byte integers. Thus, sizeof(arr) / sizeof(arr[0]) gives the wrong count. Strings decay the same way, but a string carries its own terminator, so strlen(s) walks to the \0 and needs no separate length. That holds only while the terminator survives. On a five-character source, strncpy(d, s, 5) fills d completely and writes no \0, so a later strlen(d) reads past the end.

Scope, lifetime, and storage classes

Scope says where a name is visible. Lifetime says how long its storage exists. An ordinary local has block scope and is recreated per call. A static local has block scope but whole-program lifetime.

Storage class

Meaning

auto

Default local storage, recreated on each call

static

Persists across calls, or restricts a file-scope name to its source file

extern

Declares a global object or function defined elsewhere

register

Requests fast access as a hint, rarely useful with modern compilers

int counter(void) {
    static int c = 0;
    c++;
    return c;
}

Three calls return 1, then 2, then 3. The single c is initialised once and retains its value. With int c = 0;, automatic storage makes the calls return 1, 1, 1. A static local remembers state without a global.

Recursion: how the call stack actually works

Recursion is a function calling itself. It needs a base case that stops and a recursive case moving towards it. A missing base case pushes frames until stack overflow.

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

For fact(4), calls descend through fact(3), fact(2), and fact(1). The base gives fact(1) = 1. Unwinding gives fact(2) = 2 * 1 = 2, fact(3) = 3 * 2 = 6, and fact(4) = 4 * 6 = 24. The final answer is 24.

void f(int n) {
    if (n == 0) return;
    f(n - 1);
    printf("%d ", n);
}

void g(int n) {
    if (n == 0) return;
    printf("%d ", n);
    g(n - 1);
}

f(3) prints 1 2 3 while calls return. g(3) prints 3 2 1 before each deeper call. Moving printf across the recursive call flips the order.

This same call-stack pattern drives recursive tree traversal. Binary Trees and Binary Search Trees is the natural next read for recursion over a data structure.

Call-stack diagram for fact(4) descending to the base case, then returns unwinding upward as 1, 2, 6, and 24.

When recursion gets expensive: the DP bridge

Naive recursion can repeat the same work many times.

int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

The sequence 0, 1, 1, 2, 3, 5 gives fib(5) = 5. Let C(n) count calls. Then C(0) = C(1) = 1 and C(n) = 1 + C(n - 1) + C(n - 2). Thus C(2) = 3, C(3) = 5, C(4) = 9, and C(5) = 15. The same values, including fib(3) and fib(2), are recomputed.

Memoisation stores each answer so every subproblem is solved once. Dynamic Programming Explained: 0/1 Knapsack develops that bridge. Deep recursion can overflow the stack, while divide-and-conquer sorts such as mergesort and quicksort use recursion by design.

How GATE and interviews test functions

Typical question shapes are predictable:

  • Predict recursive output. The f and g pair trains the print-order trap.

  • Trace the call stack. The factorial trace shows both descent and return.

  • Find a static local's value after several calls. The counter gives the pattern.

  • Decide whether a function changed caller state. The two swaps expose the value-copy rule.

  • Reason about a function pointer passed as a callback. A parameter declared int (*op)(int, int) holds a copied function address, so op(3, 4) with op set to add calls add and yields 7.

Functions and recursion belong to Programming and Data Structures, as the official GATE syllabus specifies. Interviews may ask for swapping without a temporary variable, recursion versus iteration, or the meaning of static in context. The GATE CS Subject Weightage breakdown shows where these ideas sit among the surrounding subjects.

The short version and your next step

  • C passes by value. Pass a pointer to modify caller state.

  • Arrays decay to pointers in parameters, so pass the length.

  • Static locals retain values across calls.

  • Every recursion needs a reachable base case.

  • Printing before versus after recursion reverses output order.

  • Naive recursion can be exponential, so memoise repeated subproblems.

Pointers in C for GATE traces the same addresses that make swapRef work, one memory diagram per dereference. Use the GATE CS Exam preparation courses for the full Programming and Data Structures track. If the language groundwork needs attention first, start with the CS Fundamentals courses.