Polyglot
Languages C functions
C § functions

Functions

A function in C is a named, parameterised, callable unit of code that returns at most one value. The language’s function model is among the simplest in commercial use: every function has a fixed list of parameters of known types, every function returns at most one value of a single type, and every call is by value. C lacks function overloading, default arguments, named arguments, closures, currying, and generics; the few generalisations the language admits — variadic arguments, function pointers, the macro _Generic — are tightly constrained and predate most of the alternatives in newer languages. The simplicity is the source of both C’s portability and its calling-convention compatibility with virtually every other systems language.

Definitions and prototypes

A function definition gives the function a body:

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

A function declaration, also called a prototype, gives the function a type without a body:

int square(int x);

A prototype declares the return type, the parameter types, and (optionally) the parameter names. It informs the compiler how to type-check calls and how to generate calls on the platform’s calling convention. Prototypes appear in headers; definitions appear in source files. A translation unit that calls a function it does not define needs a prototype to call correctly.

The compiler does not verify that the prototype seen by the caller matches the definition seen by the linker; if they disagree, the call is undefined behaviour and the diagnostic — if any — surfaces only at runtime. The conventional defence is to place the prototype in a header and #include the same header in both the calling and the defining source files, so the compiler can verify consistency.

Parameters

The list of parameters is fixed at the function’s type. Each parameter is a local variable, initialised from the corresponding argument at call time:

double area(double width, double height) {
    return width * height;
}

Parameter passing in C is by value: the function receives a copy of the argument. Modifying a parameter inside the function does not affect the caller’s variable.

void increment(int n) {
    n = n + 1;     /* affects only the local copy */
}

int x = 5;
increment(x);
/* x is still 5 */

To modify a caller’s variable, the caller passes a pointer; the function modifies what the pointer points to:

void increment(int *n) {
    *n = *n + 1;
}

int x = 5;
increment(&x);
/* x is now 6 */

The pass-by-pointer pattern is C’s mechanism for output parameters. Functions that produce more than one result conventionally return one and write the rest through pointer parameters:

bool divmod(int a, int b, int *quot, int *rem) {
    if (b == 0) return false;
    *quot = a / b;
    *rem  = a % b;
    return true;
}

Arrays as parameters

An array parameter is converted to a pointer; the syntax int arr[10] is equivalent to int *arr. The size dimension is documentation only; the function cannot determine the array’s length from the parameter type alone. The conventional remedy is to pass the length explicitly:

int sum(const int *arr, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; ++i) total += arr[i];
    return total;
}

C99 introduced variably modified array parameter syntax, which permits a parameter to be declared as int arr[n] where n is itself a parameter. The construction is more documentation than enforcement; the parameter is still passed as a pointer.

int sum(size_t n, int arr[n]) {
    /* equivalent to int sum(size_t n, int *arr); the [n] is a hint */
    int total = 0;
    for (size_t i = 0; i < n; ++i) total += arr[i];
    return total;
}

Empty parameter lists

Two forms appear:

int foo();      /* takes an unspecified number of arguments — pre-C23 */
int foo(void);  /* takes no arguments */

Before C23, an empty parameter list meant “any arguments are accepted, none are checked” — a vestige of pre-prototype K&R C. The convention to write (void) for “no parameters” was universal for portability. C23 made the empty parameter list equivalent to (void), finally retiring the legacy interpretation; portable code should still use (void) for compatibility with older compilers.

Return values

A function returns at most one value. The return type appears before the function name. A function that returns nothing has return type void:

int  add(int a, int b) { return a + b; }
void announce(const char *message) { printf("%s\n", message); }

A function with a non-void return type that reaches the closing brace without returning has undefined behaviour; the compiler should diagnose. The exception is main, where falling off the end is equivalent to return 0; (since C99).

A function may return a structure or a union, in which case the value is copied to the caller. The mechanism is well-defined but historically expensive; modern calling conventions for small structures place them in registers, and the optimiser frequently elides the copy entirely.

typedef struct { int x, y; } point;

point make_point(int x, int y) {
    point p = { x, y };
    return p;
}

point origin = make_point(0, 0);

Returning a pointer requires that the pointed-to object outlive the return. A pointer to an automatic local is invalid the moment the function exits and must not be returned:

char *bad(void) {
    char buf[64];
    return buf;        /* undefined: lifetime of buf ends with the function */
}

The conventional alternatives are: return a pointer to an allocated buffer (caller frees), to a static buffer (caller must use before the next call), or accept a caller-provided buffer as a parameter (caller manages).

Recursion

C supports recursion: a function may call itself. The conventional examples — factorial, Fibonacci, tree traversal — work as expected:

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

The depth of recursion is bounded by the size of the call stack, which is typically 1–8 MiB. Deep recursion (more than a few thousand levels for non-trivial frames) risks stack overflow; iterative reformulations or explicit-stack data structures are the conventional alternatives for unbounded depth.

The standard does not require tail-call optimisation; a recursive function whose recursive call is the last operation of the function is not guaranteed to be compiled as a loop. GCC and Clang perform tail-call optimisation when given a sufficient optimisation level, but portable code that relies on tail-call elision should be rewritten as a loop instead.

Variadic functions

A function may take a variable number of arguments, declared with ... after the fixed parameters:

int sum(int count, ...) {
    va_list args;
    va_start(args, count);
    int total = 0;
    for (int i = 0; i < count; ++i) total += va_arg(args, int);
    va_end(args);
    return total;
}

int s = sum(4, 10, 20, 30, 40);   /* 100 */

The variadic-argument access uses <stdarg.h>:

  • va_list args; declares the argument-walking state.
  • va_start(args, count); initialises it; the second argument is the name of the last fixed parameter.
  • va_arg(args, int); retrieves the next argument with the specified type.
  • va_end(args); releases the state.

The mechanism imposes severe restrictions:

  • The function cannot determine the number or types of variadic arguments from the call alone; it must derive both from the fixed parameters (a count, a format string, a sentinel value).
  • The argument types undergo the default argument promotions: char and short are promoted to int; float is promoted to double. Retrieving an argument with a different type from what was passed is undefined behaviour.
  • A mismatch between the format string and the actual arguments — common in printf-family calls — is undefined and is one of the most common security defects in C code.

The conventional printf-family functions are the principal use of the mechanism. Custom variadic functions are uncommon in modern C; the alternatives — accepting an array, accepting a struct, accepting an iterator — are more type-safe.

inline

The inline specifier (C99) is a hint that the compiler should inline calls to the function rather than generating a call. The hint is not binding; the compiler may inline non-inline functions and may not inline inline functions. The principal effect of the keyword is to permit the function to be defined in a header without violating the One Definition Rule:

/* point.h */
static inline double distance(double x1, double y1, double x2, double y2) {
    double dx = x2 - x1, dy = y2 - y1;
    return sqrt(dx * dx + dy * dy);
}

The combination static inline is the conventional form for header-defined functions: inline permits the inlining, static restricts the linkage so that each translation unit gets its own copy, and the linker does not see multiple definitions.

The bare inline keyword (without static) has more elaborate semantics: the function has external linkage, but each translation unit may inline it; one translation unit must provide a non-inline external definition for callers that take the function’s address or that the compiler chose not to inline. The static inline form sidesteps this complexity.

_Noreturn / noreturn

A function that does not return — that calls exit, that loops forever, that calls another _Noreturn — may be marked with the _Noreturn specifier (C11) or noreturn (<stdnoreturn.h>, deprecated in C23). C23 promoted the attribute syntax [[noreturn]] and deprecated the keyword forms.

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

[[noreturn]]
void fatal(const char *message) {
    fprintf(stderr, "fatal: %s\n", message);
    exit(EXIT_FAILURE);
}

The marker informs the compiler that control does not return from the call: the optimiser need not generate a return path, the diagnostic about a missing return after the call is suppressed, and the compiler may diagnose if the function does in fact return. The chief callers are error-reporting utilities, panics, and assertion-failure handlers.

static at function scope

A function defined with static at file scope has internal linkage: it is visible only within the translation unit that defines it. The convention is to declare every function static unless it appears in a header — making the symbol private to the file by default and external by exception:

/* logger.c */
static FILE *log_file = NULL;

static void open_default(void) {
    log_file = stderr;
}

void log_message(const char *msg) {     /* external: declared in logger.h */
    if (!log_file) open_default();
    fputs(msg, log_file);
}

The pattern reduces the link-time symbol table, prevents accidental name collisions, and makes the file’s interface explicit. See Scope and linkage for the full treatment.

Function pointers

A function pointer is a pointer whose pointed-to type is a function type:

int (*fn)(int);            /* fn: pointer to function taking int, returning int */

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

fn = square;
int n = fn(5);             /* 25 */

Function pointers are the principal mechanism for dynamic dispatch in C. The conventional uses are callbacks, comparators (qsort’s last argument), and method tables (vtables). The typedef form is conventional for function-pointer types because the bare syntax is easily misread:

typedef int (*comparator)(const void *, const void *);

void sort_with(int *arr, size_t n, comparator cmp) {
    qsort(arr, n, sizeof *arr, cmp);
}

The full treatment is in Pointers.

A note on overloading and defaults

C does not have function overloading: two functions with the same name and different parameter types are not permitted. A common workaround is to use a suffix indicating the type — <math.h> has fabsf (float), fabs (double), fabsl (long double) — and to have a generic-selection wrapper for callers that want type-dispatched access:

#define FABS(x) _Generic((x), \
    float:       fabsf,        \
    double:      fabs,         \
    long double: fabsl)(x)

C does not have default arguments: every parameter must be supplied at every call. The conventional substitute is multiple functions — widget_create taking a configuration struct, widget_create_default calling widget_create with sensible defaults — or initialiser-style structs that the caller fills in only as needed:

typedef struct {
    int     port;
    int     timeout_ms;
    bool    keepalive;
} server_config;

void server_start(server_config cfg);

/* caller side */
server_start((server_config){ .port = 8080 });
/* timeout_ms and keepalive default to 0 / false */

The compound-literal-with-designated-initialisers form is C99 and approximates the named-argument-with-defaults pattern of newer languages.