ZetCode

C functions

last modified November 14, 2026

A function is a named block of statements with a return type, a parameter list and a body. Calling the function transfers control to the body, binds the arguments to the parameters and returns a value to the caller. C programs are built from functions: main is the entry point, the standard library supplies ready-made functions, and the programmer writes the rest. This tutorial covers definitions and prototypes, return values, void functions, pass by value, array parameters, recursion and static functions.

Defining and calling a function

A function definition has a return type, a name, a parameter list and a body. The declaration above main, called a prototype, repeats the return type and parameter types and ends with a semicolon. The prototype lets the compiler check calls that appear before the definition; without it, the compiler has no declaration to check and C17 rejects the implicit declaration, while some compilers accept it only as an extension with a warning. The definition itself follows main and supplies the body.

function_basic.c
#include <stdio.h>

int square(int x);

int main(void) {

    int n = 7;

    printf("%d squared is %d\n", n, square(n));
    printf("%d squared is %d\n", 12, square(12));

    return 0;
}

int square(int x) {
    return x * x;
}

The call square(n) copies the value of n into the parameter x, multiplies the copy by itself and returns 49. The second call uses the constant 12 and does not touch n at all. A function can be called as many times as needed, and each call gets a fresh set of parameters.

$ gcc -std=c17 -Wall -Wextra -o function_basic function_basic.c
$ ./function_basic
7 squared is 49
12 squared is 144

Returning a value

A return statement ends the function immediately and supplies the value of the expression that follows it. A function may contain several return statements, but every path that can be reached must return a value compatible with the declared return type. The next example computes the largest of three ints with a local variable and two comparisons, then returns that variable.

function_max.c
#include <stdio.h>

int max3(int a, int b, int c) {

    int max = a;

    if (b > max) {
        max = b;
    }

    if (c > max) {
        max = c;
    }

    return max;
}

int main(void) {

    printf("max3(3, 9, 4) = %d\n", max3(3, 9, 4));
    printf("max3(-5, -2, -8) = %d\n", max3(-5, -2, -8));

    return 0;
}

The parameter list (int a, int b, int c) declares three separate copies, so the function can compare them without changing the caller's values. max starts as a, is replaced by b when b is larger, and is replaced by c when c is larger still. The result is returned as an int and printed directly in printf.

$ gcc -std=c17 -Wall -Wextra -o function_max function_max.c
$ ./function_max
max3(3, 9, 4) = 9
max3(-5, -2, -8) = -2

Functions that return nothing

A function whose only purpose is an effect on the outside world, such as printing, has the return type void. It does not return a value, and return; is allowed but optional. The parameters still have types: const char *title accepts a string literal and promises not to modify it. The two functions below print a heading and a sum, and main calls them for their side effects.

function_void.c
#include <stdio.h>

void print_line(const char *title) {
    printf("== %s ==\n", title);
}

void print_sum(int a, int b) {
    printf("%d + %d = %d\n", a, b, a + b);
}

int main(void) {

    print_line("totals");
    print_sum(4, 5);
    print_sum(-3, 8);

    return 0;
}

print_line receives the address of the first character of the literal and prints it with %s. print_sum receives two copies of its arguments and prints their sum. Neither call produces a usable value, so the calls appear as statements on their own rather than inside an expression.

$ gcc -std=c17 -Wall -Wextra -o function_void function_void.c
$ ./function_void
== totals ==
4 + 5 = 9
-3 + 8 = 5

Pass by value

C passes every argument by value: the function receives a copy, and assigning to a parameter changes only that copy. A function that must modify the caller's variable therefore receives its address, a pointer, and dereferences the pointer to reach the original object. The & operator produces an address and the unary * operator reads or writes the object at that address. The next example shows both the failed attempt and the working version.

function_swap.c
#include <stdio.h>

void swap_wrong(int a, int b) {

    int tmp = a;
    a = b;
    b = tmp;
}

void swap(int *a, int *b) {

    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {

    int x = 3;
    int y = 8;

    swap_wrong(x, y);
    printf("after swap_wrong: x = %d, y = %d\n", x, y);

    swap(&x, &y);
    printf("after swap:       x = %d, y = %d\n", x, y);

    return 0;
}

swap_wrong swaps its own parameters, which disappear when it returns, so x and y keep their values. swap receives the addresses of the two variables, and its assignments to *a and *b change the objects in main. The output shows the difference: the first call leaves 3 and 8, the second produces 8 and 3.

$ gcc -std=c17 -Wall -Wextra -o function_swap function_swap.c
$ ./function_swap
after swap_wrong: x = 3, y = 8
after swap:       x = 8, y = 3

Arrays as parameters

An array argument is not copied. It is converted to a pointer to its first element, so a function parameter written as double values[] behaves like double *values, and the function can read or modify the caller's elements. The length is not part of the pointer, so it must be passed as a separate parameter. A const pointer documents that the function only reads the array.

function_array.c
#include <stdio.h>

double average(const double values[], size_t len) {

    double sum = 0.0;

    for (size_t i = 0; i < len; i++) {
        sum += values[i];
    }

    return len == 0 ? 0.0 : sum / (double) len;
}

void scale(double values[], size_t len, double factor) {

    for (size_t i = 0; i < len; i++) {
        values[i] *= factor;
    }
}

int main(void) {

    double data[] = { 2.0, 4.0, 6.0, 8.0 };
    size_t len = sizeof data / sizeof data[0];

    printf("mean: %.2f\n", average(data, len));

    scale(data, len, 0.5);

    printf("after scale: %.2f %.2f %.2f %.2f\n",
           data[0], data[1], data[2], data[3]);

    return 0;
}

average takes a const pointer and a length, adds the elements and divides by the count. scale takes a non-const pointer and multiplies every element in place, so the change is visible in main after the call. The length is computed once in main with sizeof and passed to both functions; neither function can recover it from the pointer.

$ gcc -std=c17 -Wall -Wextra -o function_array function_array.c
$ ./function_array
mean: 5.00
after scale: 1.00 2.00 3.00 4.00

Recursion

A recursive function calls itself. Every recursion needs at least one base case that returns without another call, and a recursive case that moves the argument toward the base case. The factorial of n is the product of all integers from 1 to n, with 0! = 1; the recursive definition multiplies n by the factorial of n - 1. The next program uses unsigned long long because factorials grow quickly.

function_recursion.c
#include <stdio.h>

unsigned long long factorial(unsigned int n) {

    if (n <= 1) {
        return 1;
    }

    return n * factorial(n - 1);
}

int main(void) {

    for (unsigned int i = 0; i <= 10; i++) {
        printf("%u! = %llu\n", i, factorial(i));
    }

    return 0;
}

When n is 0 or 1 the base case returns 1 immediately. For larger values each call waits for the next one, and the multiplications happen as the calls return: 10! is 3628800. The recursion depth equals n here, so a very large argument would exhaust the call stack; the result would also overflow unsigned long long long before that.

$ gcc -std=c17 -Wall -Wextra -o function_recursion function_recursion.c
$ ./function_recursion
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800

Static functions

The static keyword on a function gives it internal linkage. The function is visible throughout its own source file but not to other translation units, so another .c file cannot call it and may define a function with the same name without a conflict. Helper functions that are not part of a module's public interface are good candidates for static. The functions below are both local to this file.

function_static.c
#include <stdio.h>

static int square(int x) {
    return x * x;
}

static int cube(int x) {
    return x * square(x);
}

int main(void) {

    for (int i = 2; i <= 4; i++) {
        printf("%d: square = %d, cube = %d\n", i, square(i), cube(i));
    }

    return 0;
}

square and cube are ordinary functions inside the file; static only changes their linkage. cube calls square, and main calls both for 2, 3 and 4. If another file in the same program also defined square, the linker would keep the two definitions separate because neither is exported.

$ gcc -std=c17 -Wall -Wextra -o function_static function_static.c
$ ./function_static
2: square = 4, cube = 8
3: square = 9, cube = 27
4: square = 16, cube = 64

Source

cppreference: function declaration
cppreference: return statement
cppreference: storage duration and linkage
cppreference: function call operator

This tutorial covered functions in C: definitions and prototypes, return values, void functions, pass by value and pointer parameters, arrays as parameters with an explicit length, recursion with a base case, and static functions with internal linkage. The C arrays tutorial explains the array-to-pointer conversion used by array parameters, C scope and storage classes covers the lifetime of the objects a function works with, and C header files shows how to share prototypes between source files.

Author

My name is Jan Bodnar, and I'm a dedicated programmer with a deep passion for coding. Since 2007, I've been sharing my expertise through over 1,400 articles and 8 e-books. With more than a decade of teaching experience, I strive to make programming accessible and engaging.

List all C tutorials.