C (programming language)

Reference entry, current to C23 (ISO/IEC 9899:2024, published 31 October 2024), with the C2y working draft described as provisional throughout. This article follows the standard entry structure described on the Overview page. Keyword, operator, header, conversion-specifier and toolchain tables are on the companion C reference page.

C is a general-purpose, statically typed, compiled programming language created by Dennis Ritchie at Bell Labs between 1969 and 1973 to rewrite the Unix kernel in something other than assembly. It is among the oldest languages in this atlas still chosen for new systems work, and the only one whose calling convention and data layout have become the neutral interchange format that other languages implement in order to talk to each other.

Three properties explain both its longevity and its risk profile. First, C exposes the storage layout of the machine: an object is a contiguous sequence of bytes, and the program can look at those bytes. Second, C leaves a large, deliberately enumerated set of situations undefined rather than either specifying a behaviour or requiring a check — this is what allows a conforming implementation to compile idiomatic C into the instruction sequence a competent assembly programmer would have written, and it is also the root of the language’s memory-safety record. Third, the language proper is small enough to be implemented by one person and stable enough that code written in 1990 still compiles: the standard has been revised six times in thirty-five years, and one of those revisions added nothing at all.

The consequence is a language that is rarely chosen for a new application and almost never removed from an existing system. Reading the landscape below in the order it is presented — machine model first, standards second, ecosystem third — is the point: nearly every argument about C, including the current one about memory safety, is an argument about the first three sections rather than about syntax.

Continue from language to system. The companion C ecosystem and systems architecture maps the full source-to-silicon toolchain, C/assembly ABI seam, Linux kernel, embedded and real-time systems, assurance and defense architecture, robotics, autonomous vehicles, and FFmpeg as one validated component graph.

Origins and the low-level bargain

C descends from a line of languages built to write compilers and operating systems on machines with a few tens of kilobytes of memory. Martin Richards’ BCPL (1967) had a single data type — the machine word — and a portable bootstrap route through an intermediate code. Ken Thompson cut BCPL down to B (1969) to fit the PDP-7. B was typeless and word-addressed, which became untenable on the byte-addressed PDP-11, so between 1971 and 1973 Ritchie added types, structures, and a preprocessor, producing C.

The decisive event was the 1973 rewrite of the Unix kernel in C. Until then, operating systems were assembly artefacts tied to one machine. A kernel written in a language that compiled to comparable code, but could be retargeted, made Unix portable, and Unix carried C with it into every research and commercial computing environment of the following decade. The 1978 first edition of Kernighan and Ritchie’s The C Programming Language served as the specification for eleven years.

What that history fixed into the language is a bargain that has never been renegotiated: the programmer states intent in terms of storage, and the implementation is not required to check that the intent was coherent. Bounds are not carried with arrays, lifetimes are not tracked, and the type of the bytes in a region is known to the compiler but not enforced at run time. In 1973 this was the only way to get an operating system out of a high-level language on a PDP-11. It remains the reason C compiles to predictable code on hardware that did not exist when the standard was written — and the reason the memory-safety debate in Trends is about C specifically.

ANSI convened X3J11 in 1983 to standardise the language against a growing set of incompatible vendor dialects; the result, C89, was adopted essentially unchanged by ISO as C90. Maintenance passed to WG14, which has held it since.

Back to top

The abstract machine

C is not defined in terms of a processor. It is defined in terms of an abstract machine in which expressions are evaluated in the order the semantics describe, and every operation happens. A conforming implementation must reproduce only the observable behaviour of that machine: the sequence of accesses to volatile objects, the data written to files at program termination, and the prompting output that precedes a blocking read. Anything else — a computation whose result is unused, a load that can be proved redundant, an entire loop — may be deleted, reordered, or replaced. This is the as-if rule, and it is the single most important sentence in the standard for understanding why a C program behaves the way it does.

Source texttranslation units, headers, macros — what the programmer editsThe abstract machinesequenced-before, observable behaviour, value computations — what the standard specifiesCompiler IRGIMPLE, LLVM IR — where the as-if rule is spent: reordering, elision, vectorisingTarget ISA and psABIinstruction selection, register allocation, calling conventionHardwarecache hierarchy, store buffers, speculative execution — outside the language entirely
Figure 1. The five levels a C program passes through. The standard defines only the top two: what a conforming implementation must preserve is the observable behaviour of the abstract machine, not the instruction sequence. Everything below the second tier is the implementation’s business, which is why the same source compiles to radically different code at -O0 and -O2 and both are correct. Source: ISO/IEC 9899:2024 §5.1.2.4. Diagram: Programming Language Atlas.

Two consequences follow immediately and account for most surprises.

Optimisation is licensed by the absence of defined behaviour, not by the presence of it. If signed overflow were defined as wrapping, then i + 1 > i would not be reducible to 1, and a loop with an int induction variable could not be promoted to the machine’s natural word size. Because overflow is undefined, the compiler is entitled to assume it does not happen, and to generate code that is wrong only for programs that were already outside the language.

Evaluation order is far weaker than the syntax suggests. C89 expressed ordering with sequence points; C11 replaced them with the sequenced-before relation, which is a partial order. In f() + g(), the two calls are indeterminately sequenced — one runs entirely before the other, but which one is unspecified and may differ between call sites. Modifying the same scalar twice with no sequencing between the modifications is undefined, which is what makes the classic i = i++ unanswerable rather than merely unspecified.

/* Evaluation order, three distinct outcomes. */
int i = 0;
a[i] = i++;            /* undefined: unsequenced modification and use of i   */
int x = f() + g();     /* unspecified order; both complete, no interleaving  */
int y = f() && g();    /* sequenced: && sequences its left operand before    */
                       /* the right, and short-circuits                      */

/* The as-if rule, visible. Both loops are observably identical, so an
   implementation may emit the closed form for the first and nothing at
   all for the second. */
unsigned long sum = 0;
for (unsigned long k = 0; k < n; k++) sum += k;   /* -> n*(n-1)/2 */
for (unsigned long k = 0; k < n; k++) ;           /* -> deleted   */

The abstract machine is also where volatile earns its keep. It has nothing to do with threads: it marks an object whose reads and writes are themselves observable behaviour, so the implementation may not add, remove, or reorder them relative to other volatile accesses. That is exactly the guarantee a memory-mapped device register needs, and exactly not the guarantee shared mutable state between threads needs — for which see the memory model.

The bottom tier of Figure 1 is where this page stops and the hardware starts, and it is worth naming what is down there. The abstract machine has no cost model: in it, every access to every object costs the same, and on the machine underneath, accesses differ in cost by two orders of magnitude depending on where the bytes happen to be. The atlas measures that tier directly, on the memory hierarchy and retrieval page.

Back to top

Objects, storage, and representation

An object in C is a region of data storage whose contents can represent values. Not a class instance: a region of bytes. Every object has a size in bytes given by sizeof, an alignment requirement, a storage duration that determines when the region exists, and an effective type that determines how the bytes may lawfully be read.

Storage durationLifetimeHow it is declaredWhere it typically lives
StaticWhole program executionstatic, file scope, string literalsZero-initialised before main; .data or .bss in the image
ThreadThe thread that owns itthread_local (C23), _Thread_local (C11)One instance per thread; needs runtime support from the loader
AutomaticEnclosing block, or the function call for parametersOrdinary local declarationsTypically the stack; indeterminate value unless initialised
AllocatedFrom allocation until freemalloc, calloc, realloc, aligned_allocThe only duration the program must manage explicitly

The four storage durations of C23. The third column names the declaration that produces each; the fourth is a description of what implementations do, not a requirement of the standard. Source: ISO/IEC 9899:2024 §6.2.4.

The representation of an object is the sequence of sizeof bytes it occupies. That sequence may contain padding bits — between structure members to satisfy alignment, at the end of a structure so that arrays of it stay aligned, and in principle inside integer types. Padding has an indeterminate value, which is why comparing two structures with memcmp is a bug rather than a shortcut, and why a structure copied field-by-field and one copied with memcpy can differ byte for byte while holding the same values.

struct packet { unsigned char kind; unsigned int length; unsigned char flags; };
/* On a typical 32-bit-int, 4-byte-aligned target:
     offset 0  kind        1 byte
     offset 1  <padding>   3 bytes   <- indeterminate
     offset 4  length      4 bytes
     offset 8  flags       1 byte
     offset 9  <padding>   3 bytes   <- so sizeof is a multiple of alignof
   sizeof(struct packet) == 12, not 6. Neither number is required by the
   standard; offsetof and sizeof are the only portable way to ask. */

_Static_assert(sizeof(struct packet) >= 6, "no upper bound is portable");

C23 finally fixed one long-standing latitude: integers are required to use two’s complement representation, ending the standard’s formal support for sign-magnitude and ones’ complement machines. What it did not do is define signed overflow — the representation is fixed, the arithmetic remains undefined. Nor did it fix the width of int, the signedness of plain char, or the number of bits in a byte, all of which remain implementation-defined and are the reason <stdint.h> exists.

Reading an object through an lvalue of the wrong type is where the effective type rule bites. An object declared with a type has that type for its whole life; an allocated object takes its effective type from the last store into it. Access through any other type is undefined, with a short list of exemptions: a compatible type, a signed/unsigned variant, a qualified version, an aggregate containing the type, and — always — a character type. That final exemption is why serialisation code is written with unsigned char and memcpy:

/* Type punning. The union form is well defined in C (and is not in C++);
   the pointer-cast form is a strict-aliasing violation and may be
   miscompiled at -O2 with no diagnostic. */
union bits { float f; uint32_t u; };
uint32_t ok(float f)  { union bits b = { .f = f }; return b.u; }

uint32_t also_ok(float f) { uint32_t u; memcpy(&u, &f, sizeof u); return u; }

uint32_t undefined(float f) { return *(uint32_t *)&f; }   /* don't */

Padding and alignment are usually met as portability hazards, which is how they are presented above. They are also the mechanism by which a C programmer decides where bytes are, and that decision is the largest lever on how fast a program runs — larger, on the hardware C actually compiles for, than the algorithm above it. The atlas takes that apart on its memory hierarchy and retrieval page, which measures the cost of a load at every level of a real machine and then measures what rearranging the same bytes is worth: a search tree whose node is one cache line beats binary search over the same keys by more than a factor of two, at the same complexity, on the same data. One section of it is about this one: exactly what the standard gives you for controlling layout, and exactly how much of the hierarchy it declines to mention.

Back to top

Pointers, arrays, and provenance

A pointer in C is not an address. It is a value that may point at an object or a function, or be null, or be indeterminate, and the rules that govern it are narrower than the arithmetic that hardware would allow. Pointer arithmetic is defined only within a single array object, and a scalar counts as an array of one. A pointer may be formed one past the end of that object, but not dereferenced there, and not two past.

int a[10];
int *p = a;            /* array-to-pointer decay: p == &a[0]                 */
int *q = a + 10;       /* one past the end: forming this is legal            */
                       /* *q is not; comparing q with a+n is                 */
ptrdiff_t n = q - p;   /* 10 -- only defined within one array object         */

int b[10];
if (a + 10 == b) { }   /* unspecified: may compare equal on a flat address   */
                       /* space, and still not be interchangeable            */

/* restrict is a promise, checked by nobody. It says: for the lifetime of
   this pointer, the object it points to is reached only through it. */
void add(size_t n, float * restrict d, const float * restrict s)
{
    for (size_t i = 0; i < n; i++) d[i] += s[i];   /* vectorisable */
}

This is stricter than “an address is an integer”, and deliberately so: the difference is what lets the compiler keep an object in a register, prove two loops touch disjoint memory, or hoist a bounds computation. It also creates the problem the committee is still working on. If a pointer that happens to hold the same address as another is not interchangeable with it, the model needs to say what travels with a pointer besides its address. That extra information is called provenance, and its absence from the standard is why round-tripping a pointer through uintptr_t, comparing pointers derived from different objects, or reconstructing a pointer from bytes has no agreed meaning today. A formal provenance model developed through the C memory-object study group is the most consequential semantic change queued for the next revision; see Future outlook.

Arrays are the other half. An array is not a pointer, and the two are conflated by a single rule: in almost every context an array expression is converted to a pointer to its first element. The exceptions are sizeof, typeof, unary &, and a string literal initialising an array. The conversion is why an array parameter is a lie — void f(int a[10]) declares a pointer parameter, the 10 is documentation, and sizeof a inside f is the size of a pointer. C99 added a way to state the intent that is honoured by static analysers and by Clang’s -fbounds-safety, but not by the language itself:

void f(size_t n, int a[static n]);   /* a shall point to at least n ints */

No bound is carried at run time. That single fact — not syntax, not manual free — is the origin of the majority of the memory-safety findings discussed under Trends.

Back to top

Undefined, unspecified, implementation-defined

C classifies everything it does not fully specify into four kinds, and the distinctions are load-bearing. They are routinely collapsed into “undefined behaviour” in conversation, which is the source of both unwarranted panic and unwarranted complacency.

ClassWhat the standard saysRepresentative casesWhat a portable program must do
UndefinedNo requirements whatsoever. The standard imposes nothing on the whole program, not merely on the construct.Signed overflow, out-of-bounds access, use after free, data race, null dereference, strict-aliasing violation, modifying a string literalThe optimiser may assume it never happens; diagnostics are optional; behaviour may differ by optimisation level
UnspecifiedTwo or more behaviours are permitted; the implementation need not document or be consistent.Order of evaluation of function arguments, order of subexpressions in f() + g(), padding byte values, the value of a + 10 == b for unrelated arraysCorrect programs must work for every permitted choice
Implementation-definedUnspecified, but the implementation must document its choice.Size of int, signedness of plain char, result of converting an out-of-range value to a signed type, >> on negative valuesPortable code must either avoid it or query it through <limits.h> and <stdint.h>
Locale-specificBehaviour depends on the current locale, which the program can select.isalpha outside the basic character set, decimal point in printf, collation orderSet the locale explicitly, or stay inside the C locale

The four classes of incompletely specified behaviour. C23 lists undefined behaviour in Annex J.2 and implementation-defined behaviour in Annex J.3; both annexes are informative and explicitly not exhaustive. Source: ISO/IEC 9899:2024 §3.4 and Annex J.

The count of distinct undefined behaviours in Annex J.2 runs into the low hundreds and has grown with every revision, because each new feature brings its own preconditions. Reading that as a defect rate misses the mechanism: undefined behaviour is how the standard declines to arbitrate between implementations, and the alternative — specifying a behaviour, such as trapping or wrapping — costs code on some target.

What has changed since the 1990s is not the rules but the consequences. An optimiser that performs interprocedural range propagation will delete a null check that follows a dereference, because the dereference already asserted the pointer is non-null. The code was always undefined; the compiler only recently became clever enough to notice. This is why the practical response is tooling rather than argument: UndefinedBehaviorSanitizer and AddressSanitizer at test time, -fwrapv or -ftrapv where wrapping semantics are genuinely wanted, and static analysis in CI. The hardening table on the reference page lists the flags.

Back to top

Translation phases and linkage

Compilation is specified as eight ordered phases. The order matters more than it looks: it is the reason a macro cannot see a comment, a line continuation works inside a string literal, and #include is textual rather than a module import.

Preprocessphases 1-4tokensCompilephases 5-7asmAssembleone object.oLinkphase 8imageLoadoutside the standard
Figure 2. The build pipeline, with the artefact crossing each boundary named on the edge. The eight translation phases of §5.1.1.2 all sit inside the first two stages; the standard has nothing to say about the last two, which is why linking errors read like a different language from compiler errors. Source: ISO/IEC 9899:2024 §5.1.1.2. Diagram: Programming Language Atlas.
PhaseWhat happensConsequence worth remembering
1Physical source characters mapped to the source character set; trigraphs replacedTrigraphs were removed in C23
2Backslash–newline splicingLine continuation happens before anything sees a token
3Decomposition into preprocessing tokens and whitespace; comments become one spaceA comment cannot be pasted around
4Directives executed, macros expanded, #included files run through phases 1–4#embed (C23) also resolves here
5Source characters and escapes converted to the execution character setWhere UTF-8 literals settle
6Adjacent string literal concatenation"a" "b" becomes one object
7Preprocessing tokens become tokens; the translation unit is translatedThe only phase most programmers picture
8Translation units and library components are collected into a program imageWhere identifiers with external linkage are resolved

The eight translation phases, in order. An implementation need only behave as if they were separate; most compilers fuse phases 3 to 7 into a single pass. Source: ISO/IEC 9899:2024 §5.1.1.2.

Phase 8 introduces the model C has instead of modules. A translation unit is one source file plus everything it includes, compiled independently. Names are joined across units by linkage: external linkage puts an identifier in one shared namespace for the whole program, internal linkage (static at file scope) confines it to one unit, and no linkage covers ordinary locals. Header files are not an interface mechanism — they are text that is pasted into each unit, which is why a declaration and its definition can disagree and the error surfaces at link time or not at all.

Two corollaries define how large C projects are built. Because each unit is compiled with no knowledge of the others, cross-unit inlining requires link-time optimisation (-flto) rather than being automatic. And because the preprocessor re-reads every header in every unit, build time scales with the product of units and header size — the pressure behind precompiled headers, unity builds, and the #embed and header-unit work in the standards committees.

Back to top

Functions: the unit of code

A function is the only unit of executable code C has. There are no methods, no closures, no nested functions in the standard language, and no module boundary smaller than the translation unit of the previous section. Everything a C program does at run time happens inside a function body, and every function is reachable, by default, from every other translation unit in the program.

Four things constitute one: a return type, an identifier, a parameter list, and a body. The first three are the function’s type and can be stated without the fourth; that separation — declaration apart from definition — is what makes separate compilation work, and it is the only interface mechanism the language provides.

/* Declaration -- a prototype. It gives the type of the call and nothing
   else: no storage, no code. Parameter names here are documentation, and
   the compiler ignores them. */
double rect_area(double length, double width);

/* Definition -- the same type, plus a body. An identifier with external
   linkage may be declared in every translation unit and defined in
   exactly one. */
double rect_area(double length, double width)
{
    return length * width;      /* a value leaves; the parameters do not */
}

/* Call -- an expression whose type is the return type, and whose operands
   are converted as if by assignment to the parameter types. */
double area = rect_area(5.5, 3.2);          /* 17.6 */

The distinction between the three forms above is worth stating precisely, because C uses one syntax for all of them. A declaration introduces a name and its type. A prototype is a declaration that also describes the parameter types, which is what makes a call checkable. A definition is a declaration with a body, and it is also a definition of the storage the function occupies. One program may contain any number of the first two and exactly one of the third.

Return types and parameter lists

Because the return type and the parameter list vary independently, function types fall into four shapes. They are usually taught as a taxonomy; they are more useful read as a statement about how much of the function’s effect the type system can see.

ShapeWhat it saysWhat it means in practiceTypical of
void f(void)Nothing in, nothing outThe whole effect is a side effect: a write to a global, a device register, a stream. The type system has nothing to check, so a misuse is invisible to the compiler.abort, an interrupt handler, a display_banner
T f(void)Nothing in, a value outReads state the caller did not pass. Not a pure function unless that state is constant — which is why two calls may legitimately differ.clock, rand, getchar
void f(T)A value in, nothing outConsumes, and reports through a pointer parameter, a global, or the outside world. Errors have nowhere to go except a channel the type does not mention.free, qsort callbacks, fputs when the result is ignored
T f(T₁, …)Values in, a value outThe only shape that composes: the result of one call is an argument to the next, and the compiler checks the join.strlen, fopen, rect_area above

The four shapes, by whether values cross the boundary in each direction. The fourth is the only one whose whole effect is visible in its type; the first three move information through channels the prototype does not mention. Source: ISO/IEC 9899:2024 §6.7.6.3 and §6.9.1.

Two details of the empty parameter list are worth keeping straight, because they changed recently. void f(void) has always meant “no parameters”. void f() meant, until C23, “the parameters are unspecified” — a declaration that switched argument checking off rather than asserting anything. C23 made the two spellings identical and removed old-style definitions entirely, closing the last route by which a call could go unchecked. Code written against C17 or earlier should still be read with the old meaning in mind.

main is the one function the standard describes rather than the program: it returns int, takes either (void) or (int, char *[]), and — uniquely — reaching the closing brace without a return is defined to return 0. That exemption is specific to main. In any other value-returning function, falling off the end is undefined as soon as the caller uses the value.

Declaration, definition, and where each belongs

A call may appear before the definition, but not before a declaration. C89 allowed a call to an undeclared function by inventing a declaration returning int, which is how a typo could become a link error rather than a compile error. C99 removed that rule along with implicit int, so a call to an undeclared identifier is a constraint violation and requires a diagnostic; C23 removed the last of the old style with it. Compilers accepted it by default for another two decades — GCC 14 and Clang 16 were the releases that finally made it an error — so a codebase that has never been built with a current compiler may still contain some.

That leaves the ordinary arrangement. A definition used in one file only is written static and needs no declaration ahead of it beyond ordering. A definition used from several files gets a prototype in a header, which every user includes, and exactly one definition in a .c file. The header is not an interface in the way a module system means it: it is text, pasted into each unit by phase 4, and if a declaration and its definition disagree the mismatch is caught at link time, or is undefined behaviour and caught by nothing. Including the header in the file that defines the function is the cheap way to have the compiler compare them.

SpecifierWhat it saysWho enforces itNotes
staticInternal linkage: the name is not visible to other translation unitsThe compiler, at translation; the linker never sees the symbolThe nearest thing C has to a private function. It also lets the compiler inline the definition freely, or delete it, because it can see every call
externExternal linkage — already the default for functionsThe linker, at phase 8Redundant on a function declaration; the habit comes from objects, where it is not
inlineA hint, plus a rule about definitionsNobody. The compiler decides for itself whether to inlineC99’s model differs from C++’s: an inline definition alone provides no external definition, which is the source of the classic “undefined reference” to an inline function
_Noreturn / [[noreturn]]This function does not return to its callerThe optimiser trusts it; returning anyway is undefinedexit, abort, longjmp-based error paths. C23 spells it as an attribute and deprecates the keyword
[[nodiscard]]Ignoring the result is probably a bugThe compiler, as a diagnostic; the program is still validC23. The answer to twenty years of ignored realloc and fread results
restrict on a parameterFor this call, the object is reached only through this pointerNobody. A false promise is undefined behaviour with no diagnosticThe reason memcpy and memmove are separate functions
const on a pointer parameterThe callee will not write through this pointerThe compiler, unless the callee casts it awayDocumentation that is checked — the cheapest interface improvement available in C

Specifiers, qualifiers and attributes that appear on a function or its parameters, and where each one is actually checked. Three of the seven are enforced by nobody: they are promises the optimiser is entitled to believe. Source: ISO/IEC 9899:2024 §6.7.1, §6.7.4, §6.7.12.

Passing parameters

A parameter is an object declared by the definition, created on entry and destroyed at the return. An argument is an expression in the parentheses of a call. The teaching vocabulary calls these formal and actual parameters; the standard’s terms are more useful because they say what each one is rather than where it appears.

C has exactly one passing mechanism: the argument is evaluated, converted as if by assignment, and copied into the parameter. There is no second mechanism to choose. “Pass by reference” in C means passing a pointer by value: the pointer is copied, the object it points at is not, and the indirection is written out by hand at every use.

/* C has one parameter-passing mechanism: copy. "Pass by reference" is the
   copy of a pointer, and the indirection is written out by hand. */

void add_ten(int n)        { n += 10; }        /* modifies the copy       */
void add_ten_p(int *n)     { *n += 10; }       /* modifies the caller's   */

int v = 5;
add_ten(v);      /* v == 5  -- the parameter was a different object */
add_ten_p(&v);   /* v == 15 -- the pointer was copied; the object was not */

/* The same rule with a bigger object is a performance decision rather than
   a semantic one: a struct argument is copied in full, so a const pointer
   is the idiom for "read this, do not copy it". */
double norm(const struct matrix *m);

/* An array parameter is a pointer parameter. All four spellings declare
   the same function, and sizeof a is the size of a pointer in every one. */
void f(int *a);
void f(int a[]);
void f(int a[10]);
void f(int a[static 10]);   /* C99: a shall point to at least 10 ints.
                               A promise to the optimiser and to analysers;
                               the language still checks nothing at run time. */

Three consequences follow from “a parameter is an object”. It is assignable, so a function may use its parameters as scratch variables without the caller noticing. Its address may be taken, and that address is not the caller’s. And the argument, being an expression, is evaluated in an order that is unspecified and unsequenced with respect to the other arguments — which is why f(i++, i++) is undefined rather than merely unpredictable, and why the classification of behaviour above is the section this belongs to.

CallerCalling conventionCalleearguments evaluated, in unspecified ordercopies placed in registers or on the stackone return value, converted to the return typethe value of the call expressionautomatic objects gone: their lifetime ended at the return
Figure 3. One call, in the order the standard fixes it. The middle actor is not part of the language: the standard says the arguments are copied into the parameters and a value comes back, and the platform ABI decides which registers and stack slots carry them. Everything the caller can observe is the value; everything the callee allocated ends its lifetime at the return. Source: ISO/IEC 9899:2024 §6.5.2.2 and §6.9.1. Diagram: Programming Language Atlas.

The middle actor in the figure is where the language hands off. The standard fixes that the values are copied and that one value comes back; it says nothing about which register carries the third argument or what happens to a sixteen-byte struct. That is the ABI layer below, and the atlas takes the x86-64 rules apart in measured detail on its calling conventions page — including the binding described here and what the copy costs once an aggregate stops fitting in registers.

Returning

A return statement returns a value, not an object. The distinction is the difference between a working function and a dangling pointer: the value is copied out before the callee’s automatic storage ends its lifetime, so returning a struct is fine and returning the address of a local is not.

/* A return statement returns a VALUE. Everything about the object it came
   from -- its address, its lifetime, its storage duration -- is left behind. */

char *bad(void)  { char buf[32]; fill(buf); return buf; }   /* dangling: buf
                                     died at the return; the pointer outlived it */

struct point far(void) { struct point p = {1, 2}; return p; }  /* fine: the
                                     struct is copied out, member by member */

/* An array cannot be returned at all -- not a limitation of the ABI but of
   the type system: there is no array-valued expression to return. The three
   ways round it are the three lifetimes a caller can be handed. */
void   into_caller(size_t n, int out[static n]);    /* caller owns the storage */
int   *from_heap(size_t n);                         /* callee allocates; caller frees */
struct vec3 by_value(void);                         /* wrap it in a struct */

/* Falling off the end of a value-returning function is not an error the
   compiler must diagnose. It is undefined behaviour only if the caller
   uses the value -- which is why -Wreturn-type is not optional. */
int maybe(int x) { if (x > 0) return 1; }           /* undefined for x <= 0 */

Arrays are the omission people notice first. A function cannot return one, because there is no array-valued expression for it to return — the same decay rule that makes an array parameter a pointer. The three replacements in the code above are not stylistic variants: each hands the caller a different lifetime, and choosing between them is the whole of C’s ownership design, made at every interface and recorded nowhere the compiler can check.

Recursion and the stack

Automatic storage duration is per invocation, not per function, so recursion needs no special support: a function that calls itself gets a fresh set of objects each time, and the pending calls form a chain that unwinds in reverse.

/* Every call creates a fresh set of automatic objects, which is the whole
   of what recursion needs: the language does not have to do anything special
   for a function to call itself. */
unsigned long long fact(unsigned n)
{
    if (n < 2) return 1;            /* base case: the only exit */
    return n * fact(n - 1);         /* one live frame per pending call */
}

/* What the standard does not give is a bound. There is no minimum recursion
   depth, no diagnostic on exceeding it, and no defined behaviour once it is
   exceeded: stack exhaustion is undefined behaviour, not an exception.
   Nor is elimination of the tail call guaranteed -- compilers do it at -O2
   and are not required to, so a depth that works in a release build can
   overflow in a debug build. */
unsigned long long fact_iter(unsigned n)
{
    unsigned long long r = 1;
    while (n > 1) r *= n--;
    return r;                        /* one frame, whatever n is */
}

What the language does not supply is a bound. There is no minimum guaranteed recursion depth, no diagnostic when it is exceeded, and no defined behaviour past that point: stack exhaustion is undefined behaviour, and on most hosted implementations it arrives as a signal rather than an error the program can handle. Nor is tail-call elimination guaranteed, so a recursion that survives at -O2 may overflow at -O0. Recursion in C is a statement about clarity, and depth is a resource the programmer has to reason about unaided — which is why kernel and embedded coding standards such as MISRA C ban it outright.

Function pointers and indirect calls

A function is not a value in C: it cannot be assigned, stored, or passed. A pointer to one can be, and the conversion from function to pointer happens implicitly almost everywhere, which is why cmp, &cmp, and *cmp all denote the same thing in a call.

/* A function is not a value in C. A pointer to one is, and the conversion
   is automatic in almost every context -- which is why all four of these
   call the same function. */
int cmp(const void *a, const void *b);

int (*p)(const void *, const void *) = cmp;   /* &cmp is the same value */
p(x, y); (*p)(x, y); (**p)(x, y); (***p)(x, y);

/* The dispatch table is the reason the type exists: it is C's vtable, its
   plugin interface, and the shape of every callback in the standard library. */
struct op { const char *name; int (*apply)(int, int); };
static const struct op ops[] = { {"add", add}, {"mul", mul} };

/* Calling through a pointer whose type is not compatible with the
   function's own is undefined -- including a cast that merely drops a
   parameter. This is a real bug class in callback registration. */
void (*handler)(void) = (void (*)(void))takes_an_int;   /* do not */

This one type carries all of C’s dynamic dispatch. A struct of function pointers is what a driver model, a plugin interface, a virtual method table, and the qsort comparator all are; C++ virtual functions and Objective-C message sends are elaborations of it rather than departures from it. The reading rule for the declarations is on the reference page under reading a declaration, because the syntax is the one part of C that genuinely cannot be read left to right.

Variadic functions

A parameter list ending in ... accepts arguments the prototype does not describe. Those arguments are not converted to a parameter type, because there is no parameter to convert them to; they get the default argument promotions instead, and the callee asserts a type for each one as it reads it.

#include <stdarg.h>

/* A variadic function's extra arguments are not described by the prototype,
   so they are not checked and not converted to a parameter type. They get
   the default argument promotions instead: float becomes double, and
   anything narrower than int becomes int. */
int sum(int count, ...)
{
    va_list ap;
    va_start(ap, count);            /* C23 also allows va_start(ap) */
    int total = 0;
    for (int i = 0; i < count; i++)
        total += va_arg(ap, int);   /* the type is asserted, not discovered */
    va_end(ap);
    return total;
}

sum(3, 1, 2, 3);        /* 6                                              */
sum(3, 1, 2);           /* undefined: va_arg past the last argument       */
sum(3, 1.0, 2.0, 3.0);  /* undefined: the doubles are read back as ints   */

Nothing about that assertion is checked. The count and the types travel out of band — in a leading count, a sentinel, or a format string — and a mismatch is undefined behaviour, which is the mechanism behind the format-string vulnerability class. The practical mitigation is not in the language: __attribute__((format(printf, ...))), honoured by GCC and Clang, teaches the compiler to check a variadic function’s format argument the way it checks printf’s. C23 softened one edge by allowing va_start with no second argument, and, for the fixed-parameter case, _Generic and macros now cover a good deal of what variadic functions were reached for — see C23.

Variadic calls are also the one place where a function’s ABI differs from its non-variadic equivalent: System V requires the caller to record the number of vector registers used, and Microsoft x64 duplicates a floating-point argument into the integer register of the same slot. The calling conventions page derives both from the same assignment rule.

Back to top

The ABI layer, where the language stops

Nothing in the C standard says which register an argument arrives in, how a structure is laid out, or what a symbol is called in an object file. All of that belongs to the platform’s processor-specific application binary interface. The standard defines source compatibility; the psABI defines binary compatibility; the two are routinely confused, and only the second determines whether a library compiled last year still links today.

The C languagetypes, linkage, the meaning of a call — ISO/IEC 9899The platform ABI (psABI)argument registers, stack alignment, struct layout, varargs, return classesObject formatELF, Mach-O, PE/COFF — symbols, relocations, sectionsLoader and linkerdynamic symbol resolution, PLT/GOT, TLS model, initialisation order
Figure 4. What has to agree for two separately compiled objects to link and run. Only the top tier is the C standard’s business; everything below is a platform document, which is why the same C source is portable while the same object file is not. Source: the named psABI documents. Diagram: Programming Language Atlas.
ABIWhere it appliesShape of the convention
System V AMD64 psABILinux, the BSDs, macOS (with Darwin deltas), Solaris on x86-64Integer args in RDI, RSI, RDX, RCX, R8, R9; floats in XMM0–7; return in RAX/RDX; 16-byte stack alignment; red zone of 128 bytes
Microsoft x64Windows on x86-64Four register args (RCX, RDX, R8, R9) regardless of class; 32-byte shadow space reserved by the caller; no red zone
AAPCS6464-bit Arm, everywhereEight integer args in X0–X7, eight vector args in V0–V7; indirect result register X8
RISC-V calling conventionRV32/RV64 Linux and embeddedEight args in a0–a7; ILP32/LP64 plus soft- and hard-float variants that are not interchangeable
Embedded / EABI variantsCortex-M, AVR, MSP430, XtensaVendor-specified; interrupt entry, register banking and startup code are outside the C standard entirely

The calling conventions a portable C project most often meets. Every row is specified by a separate document maintained outside ISO. Source: the System V AMD64 psABI, Microsoft x64 calling-convention documentation, Arm AAPCS64, and the RISC-V psABI specification.

C’s accidental monopoly follows from one property: it has no name mangling and no runtime of its own. A C function is a symbol and a calling sequence, both of which any language can produce. That is why the foreign-function interface of Python, Java, Go, Rust, Swift, Lua, Ruby, JavaScript engines, and the system-call layer of every mainstream operating system is specified in C, and why extern "C" exists in C++. When two languages that are not C need to talk, they usually agree to speak C at the boundary. Rust’s repr(C), Swift’s importer, Go’s cgo, and WebAssembly’s component-model tooling are all shaped by that decision.

The atlas takes this apart in detail on its calling conventions page, which reads the System V and Microsoft x64 assignment rules off compiled output rather than restating them: the eightbyte classification as a join over an ordered chain, and the measured cost of leaving the register path. What follows here is only what the language itself fixes.

The corollary is that the C ABI is a constraint on the C standard itself. A change that would alter struct layout or argument passing cannot be adopted, however desirable, because it would silently break every binary that already exists — which is a large part of why the language evolves as slowly as the lineage below shows.

Back to top

The memory model since C11

Before C11, threads were entirely outside the language: POSIX threads were a library, and the guarantees that made pthread_mutex_lock work were assertions by the platform rather than the compiler. C11 imported the C++11 memory model, giving the abstract machine a definition of data race — two conflicting accesses to the same object, at least one a write, neither atomic, with no happens-before between them — and declaring it undefined behaviour.

#include <stdatomic.h>
#include <threads.h>

static atomic_int ready = 0;
static int payload;                 /* plain, non-atomic */

int producer(void *unused)
{
    payload = compute();                                  /* (1) */
    atomic_store_explicit(&ready, 1, memory_order_release);/* (2) publishes (1) */
    return 0;
}

int consumer(void *unused)
{
    while (!atomic_load_explicit(&ready, memory_order_acquire))
        thrd_yield();
    use(payload);                   /* well defined: (2) synchronises with this */
    return 0;
}
Memory orderWhat it guaranteesWhere it is used
memory_order_relaxedAtomicity onlyCounters where only the final total matters
memory_order_consumeData-dependency ordering; effectively unimplemented and discouragedIntended for pointer publication on weakly ordered hardware
memory_order_acquire / _releaseA release store synchronises with an acquire load that reads itMessage passing, lock implementations
memory_order_acq_relBoth, on a read-modify-writeReference counts that must observe a predecessor’s writes
memory_order_seq_cstA single total order over all sequentially consistent operations; the defaultAnything not yet analysed — correct, and the most expensive

The memory orders of <stdatomic.h>. Colour is not used here because these are not ranked: a stronger order is not a better one, it is a more expensive one. Source: ISO/IEC 9899:2024 §7.17.3.

The primitives built on that model — condition variables, the difference between a mutex, a semaphore and a spinlock, deadlock prevention, lock-free structures and the cache effects that dominate all of them — are taken apart with measurements on the atlas’s synchronisation page. Three of the usual rules of thumb do not survive the measurement.

Two practical caveats. <threads.h> is a conditional feature: an implementation may define __STDC_NO_THREADS__ and omit it, which several have, so portable code still reaches for pthreads or Win32 threads. And atomics carry the same optionality flag, __STDC_NO_ATOMICS__ — though in practice every hosted implementation of note provides both. What the model unambiguously gave the ecosystem is a shared vocabulary: the same acquire/release semantics now appear in C, C++, Rust, and the Linux kernel’s own documented model, which made cross-language lock-free code reviewable for the first time.

Back to top

The standard lineage

C is standardised by ISO/IEC JTC1/SC22/WG14. A revision is prepared as a working draft, balloted as a committee draft, and published; between revisions, defects are answered by published defect reports that a later revision folds in. Every revision so far has been very nearly backward compatible, and the few removals have been of constructs already marked obsolescent for a decade or more.

1978K&R, 1st edition1989ANSI C891995Amendment 11999C992011C112018C17, fixes only2024C23, current
Figure 5. Six revisions in thirty-five years, plus the book that served as the specification before the first one. The gaps are the story: ten years to C99, twelve to C11, then a defect-fix-only revision, then the largest feature release since 1999. Source: ISO/IEC 9899 publication records. Diagram: Programming Language Atlas.
RevisionFormal designationPublishedKeywordsHeadersWhat it introduced
K&R CThe C Programming Language, 1st edition1978The de facto specification for a decade: no standard, no prototypes, no void *.
C89 / C90ANSI X3.159-1989; ISO/IEC 9899:19901989 / 19903215Prototypes, void, const, a specified library, and the first definition of undefined behaviour.
C95ISO/IEC 9899:1990/AMD1:199519953218Wide characters: <wchar.h>, <wctype.h>, <iso646.h>, digraphs.
C99ISO/IEC 9899:199919993724long long, _Bool, complex numbers, VLAs, restrict, inline, designated initialisers, compound literals, exact-width integers.
C11ISO/IEC 9899:201120114429A memory model: _Atomic, <stdatomic.h>, <threads.h>, plus _Generic, _Static_assert, anonymous members.
C17 / C18ISO/IEC 9899:201820184429Defect reports only. No new features; the baseline most toolchains still default to.
C23ISO/IEC 9899:202431 October 20245931constexpr, typeof, nullptr, bool as a keyword, _BitInt(N), [[attributes]], #embed, two’s complement mandated.

The six published revisions, plus the pre-standard baseline. Keyword and header counts are as listed in each revision’s own text. Source: ISO/IEC 9899:1990, its 1995 amendment, 9899:1999, 9899:2011, 9899:2018, and 9899:2024.

015304459C89C95C99C11C17C23Keywords · C89: 3232Keywords · C95: 3232Keywords · C99: 3737Keywords · C11: 4444Keywords · C17: 4444Keywords · C23: 5959Headers · C89: 1515Headers · C95: 1818Headers · C99: 2424Headers · C11: 2929Headers · C17: 2929Headers · C23: 3131KeywordsHeadersCount
Figure 6. Keywords and standard headers by revision. Both series count what the revision’s own text lists, and both are reproduced as tables on the C reference page. The C23 keyword figure includes the fourteen underscore-prefixed spellings kept for source compatibility with older code. Source: ISO/IEC 9899 §6.4.1 and clause 7 of each revision. Diagram: Programming Language Atlas.

Two revisions deserve separate mention. C99 is the one the ecosystem took longest to absorb: variable-length arrays and complex arithmetic — whose Annex G semantics the atlas takes apart on its complex arithmetic page — were expensive enough that C11 demoted both to optional features, and Microsoft’s compiler did not offer a usable C99 mode until 2013. C17 added nothing — it exists to publish accumulated defect resolutions — yet it remained the default language mode of the major compilers for years afterwards — GCC switched its default to gnu23 only in GCC 15 (2025), and Clang and MSVC still default to a C17-era mode. The practical baseline for portable code today is therefore C17 with selected C23 features guarded by __STDC_VERSION__ or __has_include.

Back to top

C23: the current standard

C23 was published as ISO/IEC 9899:2024 on 31 October 2024 — the designation and the publication year differ because the revision was named for its intended year. It is the largest feature release since C99, and its through-line is convergence: most of what it adds either already existed as a compiler extension or already existed in C++, and the committee standardised the existing spelling rather than inventing a new one.

AreaWhat C23 addsWhy it matters
Declarations and typesconstexpr objects; auto as a type deducer; typeof and typeof_unqual; _BitInt(N) for exact-width integers; enumerations with a fixed underlying type; nullptr and nullptr_tThe first C23 features to appear in shipping compilers
Keywords made ordinarybool, true, false, static_assert, thread_local, alignas, alignof are now keywords rather than macros<stdbool.h>, <stdalign.h> and <stdnoreturn.h> become obsolescent
Attributes[[deprecated]], [[fallthrough]], [[maybe_unused]], [[nodiscard]], [[noreturn]], [[reproducible]], [[unsequenced]]The C++ syntax, adopted so the two languages stop diverging on annotations
Preprocessor#embed for binary resources; #elifdef and #elifndef; __has_include and __has_c_attribute; #warning#embed removes the last common reason to run a code generator at build time
Literals and syntaxBinary literals 0b1010; digit separators 1'000'000; UTF-8 literals typed as char8_t; empty () now means (void)Old-style K&R function definitions and declarations are removed
Library<stdbit.h> bit utilities; <stdckdint.h> checked integer arithmetic; memset_explicit; strdup and strndup; %b in printf; unreachable()<stdckdint.h> is the standard’s first answer to overflow-checked arithmetic
Semantics tightenedTwo’s complement representation mandated; realloc(p, 0) made undefined; trigraphs removedThe first revision to reduce implementation latitude on integer representation

C23 by area. The list is representative rather than complete; the revision also folds in every defect report resolved since C17. Source: ISO/IEC 9899:2024, and working draft N3220 for clause numbering.

#include <stdbit.h>
#include <stdckdint.h>

constexpr int header_len = 8;             /* a constant, not a macro       */

typeof(header_len) copy = header_len;     /* deduced without naming int    */

static const unsigned char logo[] = {
#embed "logo.png"                          /* the file, as initialisers     */
};

[[nodiscard]] bool try_grow(size_t n, size_t elem, size_t *out)
{
    return !ckd_mul(out, n, elem);        /* true on success, no overflow   */
}

int leading = stdc_leading_zeros(0b0001'0000u);   /* 27 on a 32-bit uint    */

Availability, not publication, is what determines whether a feature is usable. GCC has implemented the bulk of C23 since GCC 13–15 and defaults to gnu23 from GCC 15; Clang has landed most of it across Clang 16–19 while still defaulting to a C17-era mode; MSVC ships a subset under /std:clatest. Embedded toolchains lag by years, and safety-critical projects are frequently pinned to C99 or C11 by their certification evidence. The realistic reading in 2026 is that C23 is the standard, C17 is the deployed default, and a portable file that wants C23 features tests for them:

#if __STDC_VERSION__ >= 202311L
  /* C23: bool, nullptr, [[nodiscard]] are all available */
#elif __STDC_VERSION__ >= 201112L
  #include <stdbool.h>
#endif

Back to top

Dialects, subsets, and freestanding C

Almost no large C program is written in strictly conforming ISO C. It is written in a dialect: the ISO language plus a set of compiler extensions, minus a set of constructs banned by a coding standard. Both directions matter, and they are not symmetric — the extensions are what makes systems programming practical, and the bans are what makes it auditable.

Dialect or rule setWhere it livesWhat it changesNote
GNU CGCC, and Clang under -std=gnu*Statement expressions, __attribute__, computed goto, nested functions, typeof (pre-C23), inline asmThe dialect the Linux kernel is written in; not a subset relationship with ISO C
MSVC CMicrosoft Visual C++ compiler__declspec, structured exception handling, its own __int64 family; long-incomplete C99 supportHistorically the biggest portability tax on Windows-targeting C
Clang extensionsClang/LLVMBlocks, _Nullable nullability qualifiers, __builtin_* overflow builtins, -fbounds-safetySeveral have been standardised or proposed for standardisation
Embedded CTR 18037; vendor compilersNamed address spaces, fixed-point types, hardware I/O register accessA technical report rather than part of the standard; adopted piecemeal
MISRA CSafety-critical automotive, industrialA rule set, not a dialect: bans on recursion, dynamic allocation, most of the preprocessor, and much of undefined behaviourMISRA C:2023 covers C99 through C17; enforced by static analysers
CERT CSecurity-critical workCoding rules and recommendations keyed to specific undefined behavioursComplements MISRA; both are checked by tooling rather than by the compiler
Freestanding CKernels, firmware, bootloadersNo <stdio.h>, no allocator, no main requirement; only a handful of headers must existThe mode in which most C in the world by device count is compiled

The dialects a working C programmer meets. Rows are not mutually exclusive: the Linux kernel is freestanding GNU C, and an automotive project may be MISRA-constrained freestanding C compiled by a vendor toolchain. Source: GCC and Clang extension documentation, MISRA C:2023, the SEI CERT C Coding Standard, and ISO/IEC 9899:2024 §4.

Hosted versus freestanding is the standard’s own division, and it is the one that most changes what C means in practice. A hosted implementation provides the whole library and starts at main. A freestanding implementation must provide only the headers that declare no functions — <float.h>, <limits.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, <stdint.h>, and a handful added since — and the entry point is whatever the platform says it is. Every kernel, every bootloader, and every microcontroller image is freestanding C.

The other divergence worth naming is C against C++. They stopped being a subset relationship long ago: C has designated initialisers, restrict-qualified pointers, variably modified types, and union type punning that C++ lacks or forbids; C++ has everything from overloading to templates that C does not parse. The shared surface is the ABI and the preprocessor, which is exactly the surface that extern "C" preserves. C23 narrowed some of the gap deliberately — attributes, nullptr, constexpr, bool — by adopting C++ spellings.

Back to top

The tree C produced

C’s influence runs along four separate channels that are usually conflated. Syntax is the most visible and the least consequential; the ABI is the least visible and the most consequential.

C 1972Objective-C 1984C++ 1985Perl 1987Go 2009Zig 2016Java 1995C# 2000Rust 2010
Figure 7. Direct descent and one further generation. An edge means the child’s designers cite C, or the child inherits C’s declaration syntax and semantics wholesale — not that it is compatible with C. Every node links to its record in this atlas. Source: languages.json. Diagram: Programming Language Atlas.
ChannelLanguagesWhat is inheritedWhat is not
Syntax onlyJava, C#, JavaScript, PHP, Perl, SwiftBraces, for/while, operator set and precedenceManaged memory and a runtime; no relationship to C’s object model
Syntax and semanticsC++, Objective-CThe type system, pointer arithmetic, and the preprocessor, extended rather than replacedBoth keep source-level compatibility with a large part of C, and full ABI compatibility
Reaction againstGo, Rust, Zig, DExplicitly designed to occupy C’s domain while removing a specific failure mode — unchecked aliasing, unmanaged lifetime, or the preprocessorAll three interoperate with C by speaking its ABI
Implemented in CCPython, Ruby MRI, PHP, Lua, R, Perl, most JavaScript enginesThe reference implementation, its object model, and its extension API are CTheir C API is a compatibility constraint that outlives any one version
Compiled to CNim, Vala, Cython, Chicken Scheme, historic C++ (cfront)C used as a portable assembler with an optimising backend attachedA route to every target with a C compiler, at the cost of debuggability

How C propagates. A language can appear in more than one row: Rust reacts against C’s aliasing model, is bootstrapped through LLVM rather than C, and still exposes a C-compatible ABI. Source: language specifications and reference implementations as cited on each language’s record page.

The fifth channel has no arrows on any diagram: C is the language in which the interfaces of the computing platform are written. POSIX, Win32, OpenGL and Vulkan, SQLite, zlib, OpenSSL, libcurl, the Linux system-call layer, and the CUDA and OpenCL host APIs are all specified as C headers. A language that wants to use any of them must be able to read a C declaration and honour a C calling convention, whatever else it does. That is why C’s share of new code and C’s importance to the ecosystem have been diverging for twenty years without contradiction.

Back to top

Compilers, libraries, and tooling

C has no reference implementation. The standard is the reference, and every implementation is a peer, which is why conformance is discussed in terms of documented deviations rather than compatibility with a canonical compiler.

CompilerVendor and licencePositionC-specific note
GCCGNU Project, GPL-3.0 with runtime exceptionThe reference free-software compiler; the compiler the Linux kernel and most distributions are built withDefaults to gnu23 from GCC 15; the widest target list of any C compiler
Clang / LLVMLLVM Project, Apache-2.0 with LLVM exceptionDefault on Apple platforms, FreeBSD and Android; the front end most tooling is built onSanitizers, clangd, clang-tidy and -fbounds-safety originate here
MSVCMicrosoft, proprietaryThe Windows platform compiler; C support historically trailed its C++ supportC11/C17 modes since 2020; a C23 subset under /std:clatest
Intel oneAPI (ICX)Intel, proprietary with a free tierLLVM-based since 2021; HPC and vectorisation focusReplaced the classic ICC front end
IAR, Keil, Green Hills, TI, MicrochipVendor, proprietaryEmbedded and safety-certified toolchains, often with qualification packagesFrequently pinned to C99 or C11 by certification evidence
TinyCC, chibicc, cproc, laccVarious, small permissive projectsSmall self-hosting compilers, useful for bootstrapping and teachingTinyCC compiles and runs a source file in one step

The compilers a portable C project is realistically built with. Source: GCC and LLVM release notes and C-status pages, Microsoft C language conformance documentation, and each vendor’s product documentation.

198519952005201520252035C90 — 1990-95C901990-95C99 — 1999-2013C991999-2013C11 — 2011-15C112011-15C17 — 2018-19C172018-19C23 — 2024-openC232024-open
Figure 8. Publication to broad availability, per revision. Each bar starts at publication and ends when the three mainstream desktop compilers all offered substantially complete support. Nothing here is forecast: the C23 bar runs to the present and stops open, because that revision has not reached that point yet — GCC 15 defaults to gnu23, Clang and MSVC do not. The embedded toolchain lag, longer in every case, is not shown. Source: GCC, Clang and MSVC release notes and C-conformance status pages. Diagram: Programming Language Atlas.

The library is a second, independent axis. The standard library is small; what a program actually links is a C library implementation whose POSIX surface, threading model, and symbol-versioning policy vary considerably.

ImplementationWhere it is usedCharacter
glibcGNU/Linux mainstreamComplete, POSIX-heavy, large; symbol versioning keeps old binaries running
muslContainers, Alpine, static linkingSmall, strictly standards-focused, static-link friendly
BionicAndroidSmall, tuned for the platform, with its own ABI stability rules
newlib / picolibcBare-metal and RTOS targetsFreestanding-friendly; picolibc is a newlib/AVR-libc merge tuned for small flash
uClibc-ngEmbedded Linux without MMU pressureA smaller glibc alternative for constrained systems
Microsoft UCRTWindowsShips as part of the OS since Windows 10; the API surface differs from POSIX by design
BSD libcsFreeBSD, OpenBSD, NetBSD, macOSIndependently maintained; the source of strlcpy and friends

The C library implementations in common use. Choice of libc affects binary portability far more than choice of compiler: a glibc-linked binary generally will not run against musl and vice versa. Source: project documentation for each implementation.

Tooling is where the last fifteen years of C practice actually changed. The compiler is now the smallest part of the loop: AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer and MemorySanitizer for dynamic checking; Valgrind where a rebuild is not possible; clang-tidy, Coverity, PVS-Studio and the GCC static analyser for whole-program inspection; libFuzzer, AFL++ and OSS-Fuzz for coverage-guided fuzzing; and CMake, Meson or Bazel over what was, for two decades, hand-written Make. The reference page lists the flags each of these needs.

Back to top

Where C actually runs

C’s position is unusual and is misread in both directions. It is not a growing language by any measure of new projects, and it is not a declining one by any measure of code executed. Both statements are true because they measure different populations.

DomainRepresentative systemsDialect in practiceWhy C
Operating systemsLinux, the BSDs, Windows kernel and drivers, XNU, seL4, QNX, VxWorks, Zephyr, FreeRTOSFreestanding GNU C or vendor CEffectively no replacement in production for existing kernels
Firmware and embeddedBootloaders, BIOS/UEFI, microcontroller applications, automotive ECUs, medical devicesC99/C11 under MISRA, vendor toolchainThe largest deployment by unit count; billions of parts a year
Language runtimesCPython, Ruby MRI, PHP/Zend, Lua, R, Perl, OpenJDK’s native layersHosted C with platform extensionsThe C extension API is itself a long-lived compatibility contract
Databases and storageSQLite, PostgreSQL, MySQL/MariaDB, Redis, LMDB, filesystem driversHosted C, heavy POSIX useSQLite is among the most widely deployed software of any kind
Networking and mediacurl, OpenSSL, nginx, FFmpeg, zlib, libpng, GStreamerHosted C, often with hand-written SIMDThe libraries nearly every other language links against
Numerics and acceleratorsBLAS wrappers, NumPy’s core, CUDA and OpenCL host APIs, ggml/llama.cppC with intrinsics or a C-dialect kernel languageC is the host language even where the kernels are not
ToolingGit, GNU coreutils, Vim, tmux, systemd, Wayland/X11 serversHosted CLong-lived programs where a rewrite has no funding case

Where C is the working language rather than a legacy constraint. Source: project documentation and source trees of the named systems.

SignalWhat it measuresWhat it currently saysWhat it misses
TIOBE indexRank by search-engine hit countsC has never left the top four since the index began in 2001, and held first place as recently as 2021 and 2022Measures visibility, not deployment; volatile month to month
Stack Overflow Developer SurveySelf-reported use in the past yearRoughly one respondent in five reports working with C, behind C++ and far behind Python and JavaScriptSkews toward web and application developers, who are not C’s constituency
GitHub language statisticsRepositories and pull requests by detected languageC is well outside the top five by new-repository count, and stable rather than growingCounts new public code, which is where C is weakest, and misses firmware entirely
Installed-base measuresLines in shipped systems; devices running C firmwareThe Linux kernel alone is tens of millions of lines of C, and grows every releaseThe measure that best matches where C effort actually goes

Popularity signals and their blind spots. Each row states a claim that holds across editions rather than a rank from one of them, because these figures move month to month and a quoted number dates the page the day it is published; the current edition of each index is the place to read a number. Source: the TIOBE index, the annual Stack Overflow Developer Survey, GitHub’s Octoverse reports, and Linux kernel release statistics.

The pattern behind every row is the same. C is chosen when the code must run with no runtime beneath it, when it must be callable from everything, when the target has kilobytes rather than megabytes, or when it already exists and works. It is not chosen for new application software, and has not been for twenty years.

Back to top

One subject dominates the current discussion of C, and it is not a language feature. The question is whether code written in a language with unchecked memory access can continue to sit under critical infrastructure, and the pressure is arriving through regulation and procurement rather than through language design.

TrendWhat is happeningHow to read it
Memory safety as policyGovernment and regulator guidance now names memory-unsafe languages explicitly. CISA and international partners published memory-safe roadmap guidance in 2023, the US ONCD followed in 2024, and CISA’s product-security guidance set an expectation that vendors of critical-infrastructure software publish such a roadmap by the start of 2026. The EU Cyber Resilience Act, in force since December 2024, adds product-liability obligations from December 2027.External to the language; affects procurement and product planning before it affects code
The empirical case behind itVendors that measured found memory-safety defects at around 70% of severe vulnerabilities in large C and C++ codebases. Android’s published figures show the share of memory-safety vulnerabilities falling sharply as new code moved to memory-safe languages, without rewriting the existing C.The finding that shaped the ‘new code elsewhere, old code hardened’ strategy
Hardening the C that exists_FORTIFY_SOURCE=3, -fstack-protector-strong, -ftrivial-auto-var-init=zero, control-flow integrity, shadow stacks (Intel CET, Arm PAC/BTI), and sanitizers in CI are now default practice rather than hardening exotica.The highest-return response available to an existing codebase
Bounds information in the languageClang’s -fbounds-safety, developed at Apple and contributed upstream, attaches bounds to pointers through annotations and checks them at run time. CHERI and Arm’s Morello prototype do the same in hardware with capability pointers.The most credible route to safer C without a new language
Safe C dialects and runtimesChecked C, Fil-C, and proposals such as TrapC pursue memory-safe execution of near-unmodified C source, typically trading performance or ABI compatibility for safety.Research and early production; none is a committee direction
Coexistence with RustRust support merged into the Linux kernel in 6.1 (2022) and has carried real drivers since; Android and Windows have shipped Rust components alongside C. The dominant pattern is a language boundary at the C ABI, not a rewrite.Increases the value of C’s ABI rather than reducing it
Tooling catching upCoverage-guided fuzzing, whole-program static analysis, reproducible builds, and CMake or Meson replacing hand-written Make. C still has no standard package manager, which remains its largest ecosystem gap.Where day-to-day C practice has changed most since 2010

The forces acting on C in 2026. Rows one and two are external findings; rows three to five are responses within the C ecosystem. Source: CISA and international partner guidance on memory-safe roadmaps, the US ONCD report on secure building blocks, EU Regulation 2024/2847 (Cyber Resilience Act), published Android and Chromium vulnerability analyses, LLVM documentation for -fbounds-safety, and the Rust for Linux documentation in the kernel tree.

What has not happened is equally informative. There has been no large-scale rewrite of kernels, databases or media libraries, no fork of the language, and no committee move to make C safe by default — which would break the ABI and the installed base at once. The realistic trajectory is stratified: new code at the edges moves to memory-safe languages, existing C is hardened with flags and sanitizers, the most exposed parsers are replaced individually, and the C that remains is the part where the alternative does not exist.

Back to top

Future outlook

The next revision is designated C2y. Its contents are not final: items below have been voted into the working draft or are being pursued as separate technical specifications, and either may change before publication. WG14 has not fixed a publication date, and the working pattern since C99 suggests the latter part of this decade. Anything in this section should be checked against the current WG14 document log rather than relied on.

ItemWhat it isSignificance
Provenance memory modelA formal answer to what travels with a pointer besides its address, developed through the memory-object study group and the associated technical specification workThe most consequential item: it settles arguments that today have no answer in the standard
deferLexical scope-based cleanup, pursued as a separate technical specification rather than directly in the revisionWould remove the most common use of goto cleanup in C
Named loops and labelled breakbreak and continue targeting a labelled enclosing loopAdopted into the working draft; removes another goto idiom
_CountofA standard spelling for the element count of an array, replacing the sizeof a / sizeof a[0] macroSmall, and closes a decades-old source of silent errors when an array decays
Wider constexpr and compile-time evaluationExtending C23’s constant objects toward compile-time computationUnder active discussion; scope not settled
Safety annotationsProposals to standardise bounds and nullability information that Clang already accepts as extensionsThe route by which -fbounds-safety-style checking could become portable

Work in progress for C2y and its associated technical specifications, as of the public working drafts. Provisional in every respect. Source: the WG14 document register at open-std.org/jtc1/sc22/wg14 and the published C2y working draft.

The deeper constraint on all of it is the one named under the ABI layer: C cannot adopt a change that alters layout or calling convention, and it cannot adopt a change that invalidates code written against a thirty-year-old standard, because both properties are the reason systems are written in C in the first place. That rules out most of what would make the language safe, and leaves additive, opt-in mechanisms — annotations, checked-arithmetic library functions, a provenance model that codifies what compilers already assume.

AreaExpectationVerdict
StandardisationC2y published toward the end of the decade, carrying a provenance model, cleanup facilities and further convergence with C++ spellingsExpect
DeploymentC23 becoming the deployed default as GCC 15-era toolchains reach distributions, with embedded and safety-certified toolchains lagging by yearsExpect
Safety mechanicsBounds and nullability annotations spreading through Clang, and capability hardware moving from prototype to niche deployment, ahead of any standardisationExpect
VolumeContinued growth in absolute lines of C from kernels, firmware and device count, alongside continued decline in C’s share of new projectsExpect
RegulationHardening, sanitizer use and a documented memory-safety roadmap becoming procurement requirements rather than good practiceExpect
Language safetyA memory-safe C by default, an ABI break, garbage collection, or the removal of undefined behaviour as a category — each would invalidate the installed base that is the reason to use C at allDo not expect
DisplacementRust, Zig, Swift and Go reach the system through the C ABI, which makes them consumers of C’s position at the interface layer rather than replacements for itDo not expect

A structured forecast, stated so it can be checked against later. These are the atlas’s assessments, drawn from the sources cited elsewhere on this page, not claims made by WG14.

The summary judgement: C is finished as a growth language and unfinished as an artefact. Its floor — the ABI, the system interface, the freestanding target — is not under threat from any current alternative, because every current alternative is built on it. Its ceiling is set by the memory-safety argument, which it will answer with tooling, hardening and opt-in annotations rather than with a new language. Expect the next decade of C to be about checking C, not about writing more of it.

Back to top

References and sources

  • ISO/IEC 9899:2024, Information technology — Programming languages — C, and the public working draft N3220 used for clause numbering. The normative source for every semantic claim on this page.
  • ISO/IEC 9899:1990, 9899:1999, 9899:2011 and 9899:2018, with Amendment 1:1995, for the revision history, keyword lists and header lists.
  • ISO/IEC JTC1/SC22/WG14 — the committee document register at open-std.org/jtc1/sc22/wg14, for C2y working drafts, technical specifications, and the charter.
  • Brian W. Kernighan and Dennis M. Ritchie — The C Programming Language, 1st edition 1978, 2nd edition 1988.
  • Dennis M. Ritchie — The Development of the C Language, HOPL-II, 1993, for the BCPL–B–C sequence and the Unix rewrite.
  • System V Application Binary Interface, AMD64 Architecture Processor Supplement; Microsoft x64 calling-convention documentation; Arm Procedure Call Standard for the Arm 64-bit Architecture; the RISC-V psABI specification.
  • GCC and LLVM/Clang release notes and C-status pages; Microsoft C language conformance documentation, for the compiler-availability table and figure.
  • MISRA C:2023 and the SEI CERT C Coding Standard, for the coding-rule dialects.
  • CISA and international partners, The Case for Memory Safe Roadmaps; US Office of the National Cyber Director, Back to the Building Blocks; CISA and FBI product-security guidance; Regulation (EU) 2024/2847, the Cyber Resilience Act.
  • Published Android and Chromium vulnerability analyses, for the memory-safety proportions cited under Trends; LLVM documentation for -fbounds-safety; the CHERI project publications; the Documentation/rust tree in the Linux kernel.
  • The TIOBE index, the annual Stack Overflow Developer Survey, and GitHub Octoverse reports, for the popularity signals table. Each is characterised rather than quoted, because the figures change between editions.

Code examples are original. Where this page states an expectation about the future rather than a fact — the whole of Future outlook — it is labelled as such in place. No figure on this page extrapolates: Figure 8’s open bar records that C23 has not yet reached broad availability rather than guessing when it will.

Back to top