3 min read
On this page

Function Basics

Declaration vs Definition

A function declaration (prototype) tells the compiler the function's name, return type, and parameter types. A function definition provides the actual body.

/* Declaration (prototype) */
int add(int a, int b);

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

Declarations are typically placed in header files so other translation units can call the function. Definitions go in .c files.

/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);
double average(const int *values, int count);

#endif
/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

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

Without a declaration, calling a function before its definition is an error in C99 and later.

Return Values & void

Functions return a value using return. The return type appears before the function name. Use void for functions that return nothing.

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

void greet(const char *name) {
    printf("Hello, %s!\n", name);
    /* no return statement needed, or use: return; */
}

A function declared to return a non-void type must return a value on every code path. Falling off the end without returning is undefined behavior.

/* Bug: missing return on one path */
int absolute(int x) {
    if (x < 0) return -x;
    /* falls through without return - undefined behavior */
}

Pass by Value: C Always Copies

C passes all arguments by value. The function receives a copy of each argument.

#include <stdio.h>

void try_to_modify(int x) {
    x = 999;
    printf("Inside function: x = %d\n", x);
}

int main(void) {
    int a = 42;
    try_to_modify(a);
    printf("After function call: a = %d\n", a);
    return 0;
}
Inside function: x = 999
After function call: a = 42

The original variable a is unchanged because try_to_modify only modified its local copy.

Pass by Pointer: Simulating Pass by Reference

To modify the caller's variable, pass a pointer to it:

#include <stdio.h>

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

int main(void) {
    int x = 10, y = 20;
    printf("Before: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("After:  x=%d, y=%d\n", x, y);
    return 0;
}
Before: x=10, y=20
After:  x=20, y=10

The pointer itself is still passed by value (the function gets a copy of the pointer), but the copy points to the same memory, so dereferencing it modifies the original.

A common pattern is returning a status code and writing results through pointer parameters:

int parse_int(const char *str, int *result) {
    char *end;
    long val = strtol(str, &end, 10);
    if (end == str || *end != '\0') {
        return -1;  /* parse error */
    }
    *result = (int)val;
    return 0;  /* success */
}

Variadic Functions

Functions like printf accept a variable number of arguments using ... syntax:

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

double sum(int count, ...) {
    va_list args;
    va_start(args, count);

    double total = 0.0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, double);
    }

    va_end(args);
    return total;
}

int main(void) {
    printf("Sum: %.1f\n", sum(3, 1.0, 2.5, 3.7));
    printf("Sum: %.1f\n", sum(2, 10.0, 20.0));
    return 0;
}
Sum: 7.2
Sum: 30.0

The caller must tell the function how many arguments to expect (via count, a format string, or a sentinel value). There is no type checking on variadic arguments, which makes them error-prone.

Static Functions: File-Private

The static keyword on a function restricts its visibility to the current translation unit (.c file). It has no external linkage and cannot be called from other files.

/* helpers.c */

/* Only visible within helpers.c */
static int clamp(int value, int min, int max) {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

/* Public function that uses the private helper */
int normalize_score(int score) {
    return clamp(score, 0, 100);
}

Static functions are the C equivalent of private helper functions. They prevent name collisions across files and signal that the function is an internal implementation detail.

Inline Functions

The inline keyword suggests that the compiler replace a function call with the function body:

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

In practice, modern compilers inline functions based on their own heuristics regardless of the keyword. The inline keyword matters mainly for linkage rules.

The idiomatic pattern is static inline in a header file. This gives each translation unit its own copy and avoids linker errors.

/* utils.h */
#ifndef UTILS_H
#define UTILS_H

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

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

#endif

Without static, an inline function in a header included by multiple .c files can cause linker issues. The static prevents that.

Real-World Example: A String Builder

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

struct StringBuilder {
    char *buffer;
    size_t length;
    size_t capacity;
};

static int sb_ensure_capacity(struct StringBuilder *sb, size_t needed) {
    if (sb->length + needed < sb->capacity) return 0;

    size_t new_cap = sb->capacity * 2;
    if (new_cap < sb->length + needed) new_cap = sb->length + needed + 1;

    char *new_buf = realloc(sb->buffer, new_cap);
    if (!new_buf) return -1;

    sb->buffer = new_buf;
    sb->capacity = new_cap;
    return 0;
}

struct StringBuilder *sb_create(void) {
    struct StringBuilder *sb = malloc(sizeof(*sb));
    if (!sb) return NULL;

    sb->capacity = 64;
    sb->buffer = malloc(sb->capacity);
    if (!sb->buffer) { free(sb); return NULL; }

    sb->buffer[0] = '\0';
    sb->length = 0;
    return sb;
}

void sb_destroy(struct StringBuilder *sb) {
    if (sb) {
        free(sb->buffer);
        free(sb);
    }
}

int sb_append(struct StringBuilder *sb, const char *str) {
    size_t len = strlen(str);
    if (sb_ensure_capacity(sb, len + 1) != 0) return -1;
    memcpy(sb->buffer + sb->length, str, len + 1);
    sb->length += len;
    return 0;
}

int sb_appendf(struct StringBuilder *sb, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    int needed = vsnprintf(NULL, 0, fmt, args);
    va_end(args);

    if (needed < 0) return -1;
    if (sb_ensure_capacity(sb, needed + 1) != 0) return -1;

    va_start(args, fmt);
    vsnprintf(sb->buffer + sb->length, needed + 1, fmt, args);
    va_end(args);

    sb->length += needed;
    return 0;
}

const char *sb_get(const struct StringBuilder *sb) {
    return sb->buffer;
}

int main(void) {
    struct StringBuilder *sb = sb_create();
    sb_append(sb, "Hello");
    sb_appendf(sb, ", %s! You have %d messages.", "Alice", 5);
    printf("%s\n", sb_get(sb));
    sb_destroy(sb);
    return 0;
}
Hello, Alice! You have 5 messages.

This example uses static functions for internal helpers, variadic functions for formatted appending, and pass-by-pointer throughout.

Common Pitfalls

  • Calling a function without a prototype. In C89, the compiler assumes undeclared functions return int. In C99+, this is an error. Always include the header or add a forward declaration.
  • Ignoring return values. Functions like malloc, fopen, and snprintf return values that indicate success or failure. Ignoring them leads to crashes on error.
  • Wrong variadic argument types. va_arg(args, float) is wrong because float is promoted to double in variadic calls. Use va_arg(args, double) instead.
  • Returning a pointer to a local variable. Local variables are destroyed when the function returns. The pointer becomes dangling immediately.
  • Overusing inline. Inlining large functions increases code size without improving performance. Let the compiler decide unless profiling proves otherwise.

Key Takeaways

  • Declarations go in headers; definitions go in .c files. Always prototype before calling.
  • C passes everything by value. Use pointers to modify the caller's data.
  • Static functions have file scope only, preventing name collisions and signaling internal use.
  • Variadic functions are powerful but type-unsafe. Prefer fixed-parameter alternatives when possible.
  • Use static inline in headers for small helper functions. The compiler makes its own inlining decisions regardless of the keyword.