Skip to content

C Functions, Headers, Translation Units, and Linkage

Separate compilation works because declarations let one translation unit describe entities defined elsewhere. Headers distribute those declarations; the linker later connects external symbols. Good organization keeps those views consistent.

A declaration is not necessarily a definition

c
long total_values(const int *values, size_t count); /* declaration */

The declaration tells the compiler how calls should be formed. A function definition supplies the body:

c
long total_values(const int *values, size_t count) {
    long total = 0;
    for (size_t i = 0; i < count; ++i) {
        total += values[i];
    }
    return total;
}

C passes arguments by value. A pointer value can grant access to a caller's object, but the pointer itself is still copied. To let a function replace a caller's pointer variable, pass a pointer to that pointer or return the new value.

Prototypes enable call checking

Use (void) for a C function taking no arguments:

c
void flush_report(void);

In pre-C23 C, void flush_report(); declares a function with unspecified parameters rather than a no-argument prototype. Old code may rely on non-prototype declarations; new code should not.

Variadic functions require an external type contract

Variadic functions declare at least one named parameter and use <stdarg.h>. The callee cannot infer arbitrary argument types, so an accompanying format or count contract is essential.

c
#include <stdarg.h>
#include <stddef.h>

static long sum_values(size_t count, ...) {
    va_list arguments;
    va_start(arguments, count);

    long total = 0;
    for (size_t i = 0; i < count; ++i) {
        total += va_arg(arguments, int);
    }

    va_end(arguments);
    return total;
}

Default argument promotions apply: values narrower than int arrive as int, and float arrives as double. Asking va_arg for a type incompatible with the promoted argument is undefined behavior. A count or format string must agree with the actual arguments; the language cannot verify the relationship in a general variadic call. Prefer an array plus count, a structure, or separate typed functions when the set of inputs can be modeled directly.

Use va_copy before traversing the same argument sequence independently. Every successful va_start or va_copy needs a matching va_end in that function.

Function pointers make behavior an input

A function name converts to a pointer in most expressions. The pointer type includes parameter and return types:

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

typedef int (*int_predicate)(int value, const void *context);

static size_t count_matching(
    const int *values,
    size_t count,
    int_predicate predicate,
    const void *context
) {
    size_t matches = 0;
    for (size_t i = 0; i < count; ++i) {
        if (predicate(values[i], context)) ++matches;
    }
    return matches;
}

static int at_least(int value, const void *context) {
    const int threshold = *(const int *)context;
    return value >= threshold;
}

int main(void) {
    const int values[] = {2, 5, 8, 11};
    const int threshold = 6;
    printf("%zu\n", count_matching(values, 4, at_least, &threshold));
    return 0;
}

C has no closure object built into a function pointer. A conventional void * or const void * context carries caller-owned state. The callback and context do not own one another unless the API explicitly says so. A synchronous function can borrow the address of a local threshold; an API that stores the callback must document how long the context must remain valid, which thread invokes it, and how cancellation or teardown works.

Do not cast between incompatible function-pointer types and call through the result. Calling convention and parameter representation must match the declared type. Foreign APIs may add platform-specific calling-convention annotations that belong in the public callback typedef.

Headers describe public source interfaces

c
// report.h
#ifndef REPORT_H
#define REPORT_H

#include <stddef.h>

struct report_summary {
    long total;
    size_t count;
};

struct report_summary report_summarize(const int *values, size_t count);

#endif

Include guards prevent repeated inclusion in one translation unit. #pragma once is widely supported but not standardized; projects may choose it knowingly.

Headers should include what their declarations require, remain valid when included alone, and avoid defining ordinary external objects. The implementation should include its own public header so mismatches are diagnosed.

Linkage controls identity across translation units

A file-scope function or object normally has external linkage. Adding static gives internal linkage:

c
static long clamp_total(long value) {
    return value < 0 ? 0 : value;
}

That helper belongs only to its translation unit and cannot collide with an equally named helper elsewhere.

extern commonly declares an object defined in another translation unit:

c
/* settings.h */
extern const int report_limit;

/* settings.c */
const int report_limit = 100;

Do not place the external definition in the header; every including translation unit could then define it.

Block-scope static means static storage duration, not internal linkage. The overloaded keyword names related but distinct concepts, so describe the actual property when reviewing code.

Opaque types hide representation

A public header can declare a structure tag without exposing members:

c
struct report;

struct report *report_create(void);
void report_destroy(struct report *report);

Clients can hold pointers but cannot allocate by value or access fields. The implementation owns layout and can change it without recompiling source consumers—though binary compatibility still depends on the functions' ABI and allocation contract.

Opaque handles make ownership documentation crucial: who creates, who destroys, whether null is accepted, whether functions borrow or retain pointers, and whether concurrent calls are allowed.

Preprocessor macros are textual interfaces

Macros can express conditional compilation and small generic operations, but arguments may be evaluated more than once:

c
#define BAD_MAX(a, b) ((a) > (b) ? (a) : (b))
/* BAD_MAX(i++, limit) may increment i more than once. */

Prefer functions when one type suffices. static inline functions in headers can provide type checking and avoid external definitions. C11 _Generic can select among typed functions for controlled generic interfaces.

c
static int absolute_int(int value);
static double absolute_double(double value);

#define absolute(value) _Generic((value), \
    int: absolute_int,                    \
    double: absolute_double               \
)(value)

The controlling expression of _Generic is not evaluated, but the selected function call evaluates value once. The association list must cover intended types or provide a default. Qualifiers, promotions, and array conversion can make exact matches less obvious than they look, so keep generic interfaces small and test every supported type.

Conditional compilation should select real implementation boundaries rather than scatter platform checks through domain code:

c
#if defined(_WIN32)
#include "report_windows.h"
#elif defined(__unix__) || defined(__APPLE__)
#include "report_posix.h"
#else
#error "unsupported platform"
#endif

Macros can also stringify (#) or paste tokens (##). Those operations are useful for generated declarations and diagnostics, but expansion order is subtle and identifiers created by pasting are hard for tools to follow. Prefer generated source, tables, or ordinary functions when those make the result inspectable.

Optional prompts

Debug: A global variable definition is placed in a header and the linker reports duplicates. What belongs in the header?

Answer: An extern declaration belongs in the header; exactly one translation unit should provide the external definition.

Explain: Why include a component's own header in its .c file?

Answer: The compiler can compare the public declaration with the definition in the same translation unit, catching drift in parameters, return types, and required types.

Explain: Why do many C callback APIs accept both a function pointer and void *context?

Answer: A function pointer carries executable behavior but no captured state. The context pointer lets the caller supply state explicitly, under a documented borrowing or ownership lifetime.

Further reference