The C Standards Landscape: C89 to C23

The C language keeps evolving. Slowly, deliberately, and with a strong commitment to backward compatibility — but it evolves. You cannot write “modern C” without knowing which features came in which standard, because the answer to “can I use X?” always depends on what version your target compiler supports and what platform you’re building for. This file gives you the timeline, the milestones, and the practical answer to “what do I target in 2026?”

The Timeline

Standard

Year

Common name

Notes

C89 / C90

1989/90

ANSI C, K&R2 era

The one every embedded shop still ships. Function prototypes, void, const.

C95

1995

Amendment 1

Digraphs, <wchar.h>. Minor.

C99

1999

Real modern C

// comments, mixed decls, <stdint.h>, <stdbool.h>, VLAs, designated initializers, compound literals, long long, inline.

C11

2011

Threads-and-atomics C

<threads.h>, <stdatomic.h>, _Generic, _Static_assert, anonymous struct/union, aligned_alloc, Annex K bounds-checked functions (optional, poorly supported).

C17

2018

C11 + defect reports

No new features. Bugfix release. __STDC_VERSION__ = 201710L.

C23

2024

The modern reset

bool/true/false/nullptr/static_assert as keywords, binary literals, typeof, constexpr, [[attributes]], _BitInt(N), #embed, <stdckdint.h>, mandated two’s-complement. __STDC_VERSION__ = 202311L.

What Actually Changed That Matters for You

C99 — the “you must know this” baseline

  • // comments. Trivial, ubiquitous.

  • Mixed declarations. Declare variables at first use, not just at block top.

  • <stdint.h> / <inttypes.h>. Exact-width integer types. You saw why in 01_types_and_integer_promotion.md.

  • <stdbool.h>. bool, true, false as macros. C23 promotes them to keywords.

  • Designated initializers. struct Point p = { .x = 1, .y = 2 };. Enormously useful; prevents bugs when struct fields get reordered.

  • Compound literals. f((int[]){1,2,3}, 3);. Inline array/struct construction.

  • Variable-length arrays (VLAs). Made optional in C11. Do not use.

  • inline keyword. Compiler hint (weaker than it sounds; the real inlining decision is up to the optimizer).

  • long long guaranteed 64 bits.

  • restrict qualifier. Promise to the compiler that a pointer doesn’t alias anything else in scope. Enables vectorization; use carefully.

C11 — threads, atomics, and generic programming

  • <threads.h>. Standard threading (mutex, cnd, thrd). Half-adopted — glibc supports it, macOS/BSD Clang did not for years. In 2026 support is broader but still uneven. Use POSIX pthreads for real projects; know <threads.h> exists.

  • <stdatomic.h>. Atomic types and operations. Well-supported everywhere by 2026. Use for lock-free counters and simple flags. Full memory-model literacy comes in Phase 4.

  • _Generic. Compile-time type dispatch. Enables typed macros:

    #define ABS(x) _Generic((x), \
        int: abs, long: labs, double: fabs, float: fabsf)(x)
    
  • _Static_assert(cond, msg). Compile-time check. _Static_assert(sizeof(int) == 4, "32-bit int required");.

  • Anonymous struct/union members. Covered in 03_structs_unions_bitfields.md.

  • aligned_alloc(align, size). Aligned heap allocation. Alternative on macOS: posix_memalign.

  • Annex K “bounds-checking” _s functions. Skip. Optional annex, poorly implemented, universally disliked. Use snprintf and strlcpy instead.

C17 — the boring good one

No new features. C11 with defect reports resolved. When someone says “target C17” they mean “target C11 as it was actually meant to be spec’d.” It’s the current default for GCC 11+ and Clang 11+. This is what you target for the roadmap.

C23 — the modern reset

C23 landed in 2024 as ISO/IEC 9899:2024. It’s the biggest cleanup since C99. Highlights that will affect how you write code:

  • bool, true, false are keywords. No more <stdbool.h> needed.

  • nullptr keyword. Type-safe null pointer constant. Fixes decades of NULL-is-sometimes-0-sometimes-(void*)0 confusion. Works in variadic functions where NULL doesn’t.

  • static_assert as a keyword. _Static_assert retained as alias.

  • Binary literals: 0b1010. Finally.

  • Digit separators: 1'000'000. Same as C++.

  • typeof(expr) and typeof_unqual(expr). Standardizes what GCC has had for 30 years.

  • constexpr. True compile-time constants. constexpr int MAX = 42;. Not as powerful as C++’s, but useful.

  • [[attribute]] syntax. Standardized replacement for __attribute__((...)). Includes [[nodiscard]], [[deprecated("msg")]], [[maybe_unused]], [[fallthrough]], [[noreturn]].

  • _BitInt(N). Precise-width integer of N bits, up to compiler limit. Useful for cryptography, hardware descriptions.

  • #embed "file.bin". Include binary data directly. Ends the xxd -i era.

  • <stdckdint.h>. ckd_add, ckd_sub, ckd_mul — checked overflow arithmetic in the standard library.

  • enum with explicit underlying type. enum E : uint8_t { A, B, C };.

  • auto for type inference in variable declarations. Very restricted; not like C++’s auto.

  • u8 character constants and improved UTF-8 support.

  • Mandated two’s-complement for signed integers. (One’s-complement and sign-magnitude finally banned.)

  • VLAs remain optional — the standard didn’t rescind them but they’re not required.

  • Trigraphs finally removed. Nobody will miss ??= for #.

C23 Support in 2026 Compilers

Compiler

C23 status

Flag

GCC 14

Substantial; use -std=c23 or -std=gnu23. Some features (embed) still landing.

-std=c23

GCC 15

More complete. Ships in Ubuntu 25.10.

-std=c23

Clang 18+

Reasonable coverage. -std=c23. Some features still WIP.

-std=c23

MSVC

Historically lags; check for latest status.

/std:c23 (2026)

Apple Clang

Follows upstream Clang with a delay.

-std=c23

Test support with a probe:

#include <stdio.h>
int main(void) {
#if __STDC_VERSION__ >= 202311L
    puts("C23 or later");
#elif __STDC_VERSION__ >= 201710L
    puts("C17");
#elif __STDC_VERSION__ >= 201112L
    puts("C11");
#elif __STDC_VERSION__ >= 199901L
    puts("C99");
#else
    puts("C89 or unknown");
#endif
    return 0;
}

What to Target on This Roadmap

  • Default: -std=c17. Everywhere. It’s C11 with fixes, works on every compiler you’ll touch.

  • Opt into C23 with a feature guard when you specifically want nullptr, constexpr, <stdckdint.h>, or [[nodiscard]]:

    #if __STDC_VERSION__ >= 202311L
        [[nodiscard]] int compute(void);
    #else
        int compute(void);
    #endif
    
  • Never rely on <threads.h> (uneven support), Annex K _s functions (universally disliked), or #embed (still landing in some compilers) without a fallback.

Portability Traps to Avoid

  • long is 32-bit on Windows, 64-bit on Linux/macOS. Use <stdint.h> for exact widths.

  • char signedness is platform-dependent. Signed on x86, often unsigned on ARM. Use signed char or unsigned char when the bit-pattern matters.

  • size_t printf format is %zu. Not %lu. Not %d.

  • ptrdiff_t printf format is %td.

  • Endianness — macOS and Linux on x86_64/arm64 are little-endian. Don’t assume forever, but for your Zoho/AWS/GCP targets, LE.

  • Compiler extensions. __typeof__, __attribute__, __builtin_* are GCC/Clang; not portable to MSVC without shims. Isolate them behind macros.

What Most People Get Wrong About C Standards

They either write C89 out of habit and miss 25 years of ergonomic improvements, or they chase every C23 feature and their code stops compiling on their coworker’s slightly-older toolchain. The sweet spot is C17 as the target with selective C23 opt-in behind a version guard. This is roughly what the Linux kernel, PostgreSQL, and the Zephyr RTOS all do (with variations).

The second common mistake is trusting compiler defaults. GCC’s default has drifted over the years — GCC 5 defaulted to gnu11, GCC 11+ defaults to gnu17, and future GCC will drift to newer defaults. Always specify -std= explicitly. It costs one Makefile line and saves you from silent semantic shifts across compiler upgrades.