Skip to main content
C beginner Lesson 7 of 23

Functions in C

Learn function prototypes, parameters, return values, recursion, and static functions in C.

Function Basics

Functions are the primary building block for organizing C programs. They let you name a piece of logic, reuse it from multiple places, and test it in isolation. Every function has a return type, a name, a parameter list, and a body. The main function is special — it is the entry point the OS calls when your program starts.

#include <stdio.h>

/* Function prototypes — declare signatures before use so the compiler
   can type-check calls even when definitions appear later in the file */
int add(int a, int b);
double average(double *arr, int n);

int main(void) {
    int sum = add(3, 7);
    printf("3 + 7 = %d\n", sum);

    double data[] = {4.0, 8.0, 15.0, 16.0, 23.0, 42.0};
    int len = sizeof(data) / sizeof(data[0]);
    printf("Average: %.2f\n", average(data, len));

    return 0;
}

/* Function definitions */
int add(int a, int b) {
    return a + b;
}

double average(double *arr, int n) {
    if (n <= 0) return 0.0;
    double sum = 0.0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum / n;
}

Parameters and Arguments

C passes all arguments by value — the function receives a copy of each argument. This means modifying a parameter inside the function has no effect on the caller’s variable. To let a function modify a caller’s variable, you pass its address (a pointer).

#include <stdio.h>

void try_modify(int x) {
    x = 999;   /* modifies only the local copy, not the original */
}

/* Pass a pointer so the function can modify the caller's variable */
void actually_modify(int *x) {
    *x = 999;  /* dereference the pointer to reach the original variable */
}

int main(void) {
    int val = 42;

    try_modify(val);
    printf("%d\n", val);   /* still 42 — copy was modified, not val */

    actually_modify(&val);
    printf("%d\n", val);   /* now 999 — modified through the pointer */

    return 0;
}

Multiple Return Values via Pointers

C functions can only return one value directly. When you need to return multiple results, use output parameters — pointer arguments that the function writes results into.

#include <stdio.h>
#include <stdbool.h>

/* Returns true on success; writes quotient and remainder via output params */
bool divide(int a, int b, int *quotient, int *remainder) {
    if (b == 0) return false;   /* signal error without crashing */
    *quotient  = a / b;
    *remainder = a % b;
    return true;
}

int main(void) {
    int q, r;
    if (divide(17, 5, &q, &r)) {
        printf("17 / 5 = %d remainder %d\n", q, r);
    }
    return 0;
}

void Functions

A function with return type void performs an action but produces no result. Use return; (with no value) to exit early when a condition makes further execution pointless.

#include <stdio.h>

void print_separator(char ch, int width) {
    for (int i = 0; i < width; i++) {
        putchar(ch);
    }
    putchar('\n');
}

void print_table(int rows, int cols) {
    if (rows <= 0 || cols <= 0) return;   /* early exit — nothing to print */

    print_separator('-', cols * 5);
    for (int r = 1; r <= rows; r++) {
        for (int c = 1; c <= cols; c++) {
            printf("%4d ", r * c);
        }
        printf("\n");
    }
    print_separator('-', cols * 5);
}

int main(void) {
    print_table(5, 5);
    return 0;
}

Recursion

A recursive function calls itself to solve a smaller version of the same problem. Recursion is elegant for problems that have a naturally recursive structure — trees, divide-and-conquer algorithms, mathematical sequences. Every recursive function must have a base case that stops the recursion; without it, the function calls itself forever until the stack overflows.

#include <stdio.h>

/* Factorial: n! = n * (n-1) * ... * 1, base case: 0! = 1 */
long long factorial(int n) {
    if (n <= 1) return 1;          /* base case — stop recursing */
    return n * factorial(n - 1);  /* recursive case — solve smaller problem */
}

/* Fibonacci: fib(n) = fib(n-1) + fib(n-2), base cases: fib(0)=0, fib(1)=1 */
int fibonacci(int n) {
    if (n <= 0) return 0;
    if (n == 1) return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

/* Tower of Hanoi: move n disks from 'from' to 'to' using 'via' as intermediary */
void hanoi(int n, char from, char to, char via) {
    if (n == 0) return;
    hanoi(n - 1, from, via, to);               /* move n-1 disks out of the way */
    printf("Move disk %d from %c to %c\n", n, from, to);
    hanoi(n - 1, via, to, from);               /* move n-1 disks to destination */
}

int main(void) {
    for (int i = 0; i <= 10; i++) {
        printf("%d! = %lld\n", i, factorial(i));
    }
    printf("\nHanoi with 3 disks:\n");
    hanoi(3, 'A', 'C', 'B');
    return 0;
}

Tail recursion is a special case where the recursive call is the very last operation. Some compilers optimize it into a loop (tail call optimization), eliminating the stack growth:

/* Non-tail-recursive: the multiplication happens AFTER the recursive call returns */
long long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

/* Tail-recursive: accumulator carries the result, recursive call is last */
long long factorial_tail(int n, long long acc) {
    if (n <= 1) return acc;
    return factorial_tail(n - 1, n * acc);  /* last operation — eligible for TCO */
}

/* Public wrapper that hides the accumulator from callers */
long long fact(int n) { return factorial_tail(n, 1); }

Static Functions

A static function is only visible within the file it is defined in. This is C’s mechanism for making functions private to a module — hiding implementation details from other files prevents accidental use and name collisions across a large codebase.

/* math_utils.c */
#include "math_utils.h"
#include <math.h>

/* Private helper — not callable from other .c files */
static double clamp(double val, double min, double max) {
    if (val < min) return min;
    if (val > max) return max;
    return val;
}

/* Public API — declared in math_utils.h, callable from anywhere */
double safe_sqrt(double x) {
    double clamped = clamp(x, 0.0, 1e308);  /* ensure x is non-negative */
    return sqrt(clamped);
}

Use static for any function that is only needed internally. It documents intent clearly and lets the compiler optimize more aggressively.

Inline Functions (C99)

inline hints to the compiler that the function body should be substituted at the call site, eliminating function call overhead. This is useful for very small, frequently called utility functions. Prefer static inline over macros because you get type checking and proper scoping.

#include <stdio.h>

static inline int max(int a, int b) {
    return (a > b) ? a : b;
}

static inline int min(int a, int b) {
    return (a < b) ? a : b;
}

/* Compose smaller inline functions to build more complex ones */
static inline int clamp_int(int val, int lo, int hi) {
    return max(lo, min(val, hi));
}

int main(void) {
    printf("%d\n", clamp_int(150, 0, 100));  /* 100 — clamped to max */
    printf("%d\n", clamp_int(-5,  0, 100));  /* 0   — clamped to min */
    printf("%d\n", clamp_int(50,  0, 100));  /* 50  — within range */
    return 0;
}

Variadic Functions

Variadic functions accept a variable number of arguments, like printf does. The <stdarg.h> macros give you access to the extra arguments at runtime. The caller and the function must agree on the types and count — C has no runtime type introspection, so getting it wrong causes undefined behavior.

#include <stdio.h>
#include <stdarg.h>

/* Sum exactly 'count' integers passed as variadic arguments */
int sum(int count, ...) {
    va_list args;
    va_start(args, count);  /* initialize args after the last named parameter */

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);  /* retrieve next argument as int */
    }

    va_end(args);   /* clean up — required before returning */
    return total;
}

int main(void) {
    printf("%d\n", sum(3, 10, 20, 30));        /* 60  */
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));    /* 15  */
    return 0;
}

The standard library’s printf is a variadic function. Building your own requires care: the caller must communicate the count and types of extra arguments (like printf uses the format string), because C has no runtime type introspection.

Frequently Asked Questions

What is a function prototype and why do I need it?
A prototype declares a function's signature before its definition, so the compiler can type-check calls made before the function body appears. Without it, the compiler assumes the function returns int and accepts any arguments.
Does C support default parameter values?
No. C functions require all arguments to be passed explicitly. You can simulate defaults using wrapper functions or variadic functions, but there is no language-level default parameter feature.
What is the difference between pass by value and pass by reference in C?
C is always pass by value — a copy of the argument is made. To simulate pass by reference (so a function can modify the caller's variable), pass a pointer to the variable.