The C Path — learn C, visually

🧬 Types & Qualifiers, In Depth

inline: a hint, a header trick, and a weird rule

⏱ 12 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

A game redraws the screen 60 times a second, calling tiny helpers like max() millions of times — and merely calling them can cost more than the work they do. inline is C's tool for making tiny functions effectively free. Along the way it explains a baffling "undefined reference" error that you are otherwise destined to meet someday.

Calling a function costs a little: arguments get arranged, a jump happens, a stack frame — the function's private patch of stack memory — is set up. For a three-instruction function called a billion times, the overhead can dwarf the work. Inlining — pasting the body directly where the call was written — removes the overhead and, more importantly, lets the optimizer melt caller and callee together.

What the keyword actually does (little!)

Here's the modern reality: inline is only a suggestion, and at -O2 compilers inline aggressively with or without it — and refuse when it's a bad idea, with or without it. The keyword's real, practical value is different: it changes the rules so a function definition can legally live in a header included by many .c files, without "multiple definition" linker errors.

imath.h — the pattern to memorize
#ifndef IMATH_H
#define IMATH_H

/* safe to #include from any number of .c files */
static inline int imax(int a, int b) {
    return a > b ? a : b;
}

static inline int iclamp(int x, int lo, int hi) {
    return imax(lo, x < hi ? x : hi);
}

#endif
terminal
$ gcc -O2 -c a.c b.c c.c   # all three include imath.h
$ gcc a.o b.o c.o -o app   # no "multiple definition" errors
# and at -O2, calls to imax compiled to a single cmov — no call at all

Why static inline? Each translation unit gets its own private copy of the function; the copies never meet at link time, so there's no conflict. The optimizer inlines the calls and usually no copy even exists in the final binary.

🧠 Checkpoint: Why does static inline in a header not cause "multiple definition" linker errors?

  • inline deletes the function entirely
  • static gives each translation unit its own internal copy — the linker never sees a clash
  • The preprocessor removes duplicate definitions
  • Headers are only compiled once
Show answer

static gives each translation unit its own internal copy — the linker never sees a clash — static = internal linkage: each .c that includes the header owns a private version. Private symbols are not matched across TUs, so no conflict — and unused copies are simply discarded.

The weird C99 rule (told honestly)

Plain inline — without static — has famously confusing semantics. Watch what happens:

lonely.c — plain inline, no extern anywhere
#include <stdio.h>

inline int twice(int x) { return 2 * x; }

int main(void) {
    printf("%d\n", twice(21));
    return 0;
}
terminal
$ gcc -O0 lonely.c
/usr/bin/ld: /tmp/ccXig1zw.o: in function 'main':
lonely.c:(.text+0xe): undefined reference to 'twice'
collect2: error: ld returned 1 exit status
# at -O2 it "works" — the call was inlined so no symbol was needed. Fragile!

What?! Here's the rule, as simply as it can be put: a function defined with only inline provides an inline definition — a body the compiler may use for inlining, but which does not emit an actual, linkable function. If the compiler chooses to make a real call (it always does at -O0), the linker needs a real function… which nobody emitted. To emit one, exactly one translation unit must add an extern declaration:

the C99-correct fix (one TU only)
/* twice.h — inline definition, included everywhere */
inline int twice(int x) { return 2 * x; }

/* twice.c — exactly ONE file forces a real, linkable copy */
#include "twice.h"
extern int twice(int x);   /* "emit the external definition here" */
⚠️

Extra confusion, historical edition: pre-C99 GNU C ("gnu89 inline") used the same keywords with the meanings flipped. If you read old code or blog posts, check which dialect they mean. This mess is why the ecosystem settled on the pattern below.

The practical decision

This spot has an interactive flow widget — open the interactive lesson to play with it.

Rule of thumb that ends all confusion: for small functions in headers, write static inline and move on with your life. Reserve C99's extern inline dance for libraries that need one canonical out-of-line copy (glibc does this; you almost never need to).

🧠 Checkpoint: A function defined with plain inline (no static, no extern declaration anywhere) links fine at -O2 but fails at -O0. Why?

  • -O0 disables the inline keyword
  • An inline definition emits no linkable symbol; -O2 inlined every call (no symbol needed), -O0 emitted real calls to a function that does not exist
  • The linker requires optimization for inline
  • It is a compiler bug
Show answer

An inline definition emits no linkable symbol; -O2 inlined every call (no symbol needed), -O0 emitted real calls to a function that does not exist — That is the C99 rule in action: inline-only = "body available for inlining, but I am not the external definition". Whether you need the symbol depends on whether calls were actually inlined — hence the optimization-level-dependent link error.

When inlining hurts

Inlining duplicates code. Inline a 200-line function into 50 call sites and your binary balloons — and a bigger binary means more instruction-cache misses, which can make the program slower than honest calls. Compilers weigh this with size heuristics; trust them. Profile first, and if a hot call really must vanish, check the compiler agreed (look at the assembly, or use -Winline) rather than sprinkling inline as a magic performance spice.

💡

Since inlining requires the body to be visible in the calling translation unit, functions hidden in other .c files can't be inlined — unless you enable link-time optimization (-flto), which lets the "linker" re-optimize across files. More in Part 8.

🧠 Checkpoint: Inlining a large function into many call sites can make a program slower because…

  • inline functions cannot use registers
  • the duplicated code enlarges the binary and thrashes the instruction cache
  • inlined code cannot be optimized
  • each copy re-checks its arguments
Show answer

the duplicated code enlarges the binary and thrashes the instruction cache — Code bloat is the classic inlining downside: more bytes of machine code competing for the same small i-cache. This is why compilers apply size heuristics and why "inline everything" is an anti-pattern.

This spot has an interactive editor widget — open the interactive lesson to play with it.

inline asks the compiler to optimize harder; the next keyword, restrict, gives it permission to — by promising your pointers don't overlap.

▶ Practice this lesson interactively (with live gcc)