Functions

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

A C function has a name, a return type, a fixed parameter list and a body. Everything is passed by value — including pointers and structs — and there is no overloading, no default arguments and no closures. That austerity is why C’s calling convention became the interface every other language links against.

Prototypes and Definitions

A prototype declares the name, return type and parameter types; a definition also provides the body:

#include <stdio.h>

double average(const int *values, size_t n);        // prototype -- note the parameter types

int main(void)
{
    int data[4] = { 1, 2, 3, 4 };
    printf("%.2f\n", average(data, 4));
    return 0;
}

double average(const int *values, size_t n)         // definition
{
    if (n == 0) {
        return 0.0;
    }

    long long total = 0;
    for (size_t i = 0; i < n; ++i) {
        total += values[i];
    }
    return (double)total / (double)n;
}

The prototype is what lets the compiler check the call. Without one, C89 assumed an int return and unchecked arguments; C23 finally removed that rule — calling an undeclared function is now an error, and old-style (K&R) parameter lists are gone.

One C23 change worth knowing: void f() and void f(void) now mean the same thing ("no parameters"). Before C23, () meant "unspecified parameters" and disabled argument checking, which is why every older codebase writes (void) religiously. Keep writing (void) while you still support C17.

Everything Is Pass-by-Value

The parameter is a copy. To let a function modify a caller’s object, pass its address:

#include <stdio.h>

static void does_nothing(int value)
{
    value = 99;                     // modifies the local copy only
    printf("inside does_nothing: %d\n", value);     // 99 -- but the caller's x is untouched
}

static void doubles_it(int *value)
{
    *value *= 2;                    // modifies the caller's object
}

static void swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void)
{
    int x = 5, y = 7;

    does_nothing(x);
    doubles_it(&x);
    swap(&x, &y);

    printf("%d %d\n", x, y);        // 7 10
    return 0;
}

The same is true of structs — they are copied wholesale, which for a large struct is real work. Pass const struct Big * instead of struct Big when the struct is more than a couple of words, and note that passing a small struct by value is perfectly idiomatic and often faster.

Array Parameters

An array parameter is silently rewritten as a pointer — there is no way to pass an array by value:

#include <stddef.h>
#include <stdio.h>

// These three declare exactly the same function:
static int sum_a(int values[10], size_t n);
static int sum_b(int values[], size_t n);
static int sum_c(int *values, size_t n);

static int sum_c(int *values, size_t n)
{
    int total = 0;
    for (size_t i = 0; i < n; ++i) {
        total += values[i];
    }
    return total;
}

// C99: [static n] documents (and lets the compiler assume) at least n elements,
// and turns a null argument into a diagnosable error.
static int sum_checked(size_t n, int values[static 1])
{
    int total = 0;
    for (size_t i = 0; i < n; ++i) {
        total += values[i];
    }
    return total;
}

// A variably-modified parameter: the length is a real parameter, so 2-D indexing works.
static int sum_matrix(size_t rows, size_t cols, int matrix[rows][cols])
{
    int total = 0;
    for (size_t r = 0; r < rows; ++r) {
        for (size_t c = 0; c < cols; ++c) {
            total += matrix[r][c];
        }
    }
    return total;
}

int main(void)
{
    int values[4] = { 1, 2, 3, 4 };
    int grid[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };

    printf("%d %d %d\n", sum_c(values, 4), sum_checked(4, values), sum_matrix(2, 3, grid));
    return 0;
}

The consequence: sizeof values inside the function is the size of a pointer, not of the array. Always pass the length alongside the pointer — that convention is the whole reason C’s string and memory functions take an n.

Why main Is Special

main is the only function with a fixed set of allowed signatures, the only one called by the runtime rather than by your code, and the only one where falling off the end means return 0. It also may not be called recursively in a strictly conforming program. See Program Structure.

Recursion

Every function may call itself; each call gets its own frame of automatic objects.

Call stack for factorial(4): four stacked frames each holding its own n
#include <stdio.h>

static unsigned long long factorial(unsigned n)
{
    if (n <= 1) {
        return 1;                       // base case first -- always
    }
    return n * factorial(n - 1);        // one recursive call: naturally a loop
}

// Tail-recursive form: nothing to do after the call, so the compiler can turn
// it into a jump at -O2. C does not guarantee this -- it is an optimization.
static unsigned long long factorial_tail(unsigned n, unsigned long long acc)
{
    if (n <= 1) {
        return acc;
    }
    return factorial_tail(n - 1, acc * n);
}

// Recursion earns its keep on recursive data, not on arithmetic:
struct Node { int value; struct Node *left, *right; };

static int tree_sum(const struct Node *node)
{
    if (node == nullptr) {
        return 0;
    }
    return node->value + tree_sum(node->left) + tree_sum(node->right);
}

int main(void)
{
    struct Node leaf_l = { .value = 1 };
    struct Node leaf_r = { .value = 3 };
    struct Node root = { .value = 2, .left = &leaf_l, .right = &leaf_r };

    printf("%llu %llu %d\n", factorial(20), factorial_tail(20, 1), tree_sum(&root));
    return 0;
}

C gives you no stack-depth guarantee: deep or unbounded recursion is stack exhaustion, which is undefined behavior and usually a crash. Bound the depth, or rewrite as a loop with an explicit stack, when the input is attacker-controlled.

static Functions — Internal Linkage

static on a function means this translation unit only. It is the closest thing C has to a private method:

#include <stdio.h>

static int helper(int x)        // not visible to the linker: no name clashes, freely inlinable
{
    return x * 2;
}

int public_api(int x);          // external linkage: declared in this project's header

int public_api(int x)
{
    return helper(x) + 1;
}

int main(void)
{
    printf("%d\n", public_api(20));
    return 0;
}

Make every function static unless a header declares it. It shrinks the symbol table, lets the optimizer inline and specialize freely, and turns "who calls this?" into a question one file can answer.

inline Functions

inline is a hint about linkage, not a command to inline. The rules trip up almost everyone, so here is the whole story in two idioms:

small.h
#ifndef SMALL_H
#define SMALL_H

// 1. static inline in a header: every TU gets its own private copy.
//    This is the one to use by default -- no separate definition needed.
static inline int min_int(int a, int b)
{
    return a < b ? a : b;
}

// 2. plain inline in a header requires exactly ONE external definition
//    in some .c file (see small.c below), or the link fails.
inline int max_int(int a, int b)
{
    return a > b ? a : b;
}

#endif /* SMALL_H */
small.c
#include "small.h"

extern int max_int(int a, int b);       // the one external definition of the inline function
#include "small.h"
#include <stdio.h>

int main(void)
{
    printf("%d %d\n", min_int(3, 4), max_int(3, 4));
    return 0;
}

In practice: use static inline in headers, let the compiler decide (it ignores your hint at -O0 and inlines plenty of non-inline functions at -O2), and reach for __attribute__((always_inline)) only with a measurement in hand. See Performance.

[[noreturn]]

A function that never returns to its caller should say so — it lets the compiler drop the return path and stops "control reaches end of non-void function" warnings at the call site:

#include <stdio.h>
#include <stdlib.h>

[[noreturn]] static void fatal(const char *msg)
{
    fprintf(stderr, "fatal: %s\n", msg);
    exit(EXIT_FAILURE);                 // must not return -- UB if it ever does
}

static int must_be_positive(int n)
{
    if (n <= 0) {
        fatal("expected a positive value");
    }
    return n;                           // the compiler knows fatal() never comes back
}

int main(void)
{
    printf("%d\n", must_be_positive(5));
    return 0;
}

[[noreturn]] is the C23 spelling; C11 had _Noreturn and the <stdnoreturn.h> macro noreturn, both now deprecated. exit, abort, _Exit, quick_exit and thrd_exit are all declared this way in the standard library.

Variadic Functions — <stdarg.h>

A function can take a variable number of arguments after at least one named parameter. C gives you no way to know how many — you must pass a count or a sentinel:

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

// Count-terminated: the caller says how many follow.
static int sum_of(int count, ...)
{
    va_list args;
    va_start(args, count);              // C23: va_start(args) also works

    int total = 0;
    for (int i = 0; i < count; ++i) {
        total += va_arg(args, int);     // the type must match what was passed
    }

    va_end(args);                       // mandatory
    return total;
}

// Sentinel-terminated: a null pointer marks the end.
static void print_all(const char *first, ...)
{
    va_list args;
    va_start(args, first);

    for (const char *s = first; s != nullptr; s = va_arg(args, const char *)) {
        printf("%s ", s);
    }

    va_end(args);
    putchar('\n');
}

// Forwarding to a v-prefixed library function -- the only way to wrap printf.
static void log_message(const char *format, ...)
{
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);     // vprintf/vsnprintf/vfprintf take a va_list
    va_end(args);
}

int main(void)
{
    printf("%d\n", sum_of(4, 1, 2, 3, 4));
    print_all("a", "b", "c", nullptr);
    log_message("%s=%d\n", "answer", 42);
    return 0;
}

The pitfalls are all about types, because default argument promotions apply to the variadic arguments:

  • char, short and bool arrive as int; float arrives as double. So va_arg(args, float) is always wrong — ask for double.

  • A mismatch between va_arg’s type and what was actually passed is undefined behavior, and nothing checks it. This is exactly why `printf format bugs are dangerous; add __attribute__((format(printf, 1, 2))) to your own wrappers so GCC/Clang check them.

  • va_list may be consumed only once; va_copy gives you a second pass, and needs its own va_end.

  • Use a _Generic dispatch or a tagged-union parameter instead when the argument types are known — see Type-Generic Programming.

See Also