JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Beyond C89: What C11, C17, and C23 Changed

The C you learned in the earlier rungs is the timeless core — but the language kept evolving. Here is the map of what C11, C17, and C23 actually added, and why a modern dialect makes your code safer and sharper.

One language, many standards

You already write C that works. So why a whole rung on "modern C"? Because the C you met in the earlier rungs is, deliberately, the timeless core: pointers, the stack and heap, structs, the preprocessor. But C is not frozen. It is governed by a written language standard that a committee revises every few years, and each revision quietly adds tools that make the same programs safer, clearer, or faster to compile. This guide is the map. The four guides after it zoom into the features; this one gives you the lay of the land so the rest has somewhere to land.

The name "C89" comes from the year the language was first standardized: 1989, by ANSI (the ISO version, identical in content, is dated 1990, so you also see "C90"). That is the dialect most textbooks teach because it runs almost everywhere. After it came C99 (which you have probably been using without noticing — that is where // comments, declaring a variable mid-block, and the bool from <stdbool.h> come from), then the three this rung cares about: C11, C17, and C23.

C89 / C90  (1989/1990) ---- the timeless core
C99        (1999)      ---- // , mid-block decls, <stdbool.h>, long long
C11        (2011)      ---- _Generic, _Static_assert, _Alignas, _Noreturn, threads
C17 / C18  (2017/2018) ---- a bug-fix release: no new features, clarifications
C23        (2024)      ---- bool/true/false keywords, nullptr, [[attributes]], typeof, constexpr
The line of C standards. C17 added nothing new on purpose; C11 and C23 carry the features this rung studies.

C11: the big one

C11 is where modern C really begins, and most of this rung lives here. Four additions matter for everyday systems code. First, _Generic: a way to pick an expression at compile time based on the type of its argument, which finally lets a single macro behave differently for int x, double x, or char *s without writing one function per type. Guide 2 of this rung is devoted to it.

Second, _Static_assert: an assertion the compiler checks while compiling, not while running. Write _Static_assert(sizeof(int) == 4, "need 32-bit int") and if it is false, the build stops with your message — a bug caught before the program ever exists. Third, _Alignas and _Alignof: you can now state and query the memory alignment of a type in portable C, instead of relying on per-compiler tricks. Both are the subject of guide 3.

Fourth, C11 added an optional standard threads library, <threads.h>, and atomic types in <stdatomic.h> — the same concurrency machinery you met as POSIX pthreads in the earlier rungs, but now blessed by the language itself. (In practice many platforms still ship pthreads more reliably than <threads.h>, so this rung leans on what you already know rather than the C11 thread API.) C11 also made _Noreturn a keyword, the spelling behind functions like exit() that never come back.

Those underscore-capital names

You have surely noticed the ugly spellings: _Generic, _Static_assert, _Alignas, _Noreturn, _Bool. There is a reason, and it teaches you something real about how a standard grows without breaking the world. The C standard reserves all identifiers that begin with an underscore followed by a capital letter for the language itself. Ordinary programmers are not allowed to name things that way. So when the committee needs a brand-new keyword, it can grab one of those reserved names and be certain no existing program already used it as a variable — guaranteeing old code keeps compiling.

But _Static_assert is hideous to read. So the standard pairs many of these with a friendly macro in a header. Include <assert.h> and you can write static_assert instead of _Static_assert; include <stdalign.h> for alignas and alignof; include <stdbool.h> for bool, true, false; include <stdnoreturn.h> for noreturn. The lowercase name is just the keyword in a nicer suit. Knowing this demystifies a lot of "magic": when you see alignas in someone's code, you now know it is C11's _Alignas with a header pulled in.

C17 cleaned up; C23 modernized the surface

C17 (also written C18) is the easiest standard to summarize: it added no new features. It is a maintenance release that fixed defects and ambiguities in C11's wording. That is worth knowing precisely because it is a non-event — if a colleague says "we target C17", they mean "C11, but with the spec's bugs ironed out". Nothing you learn about C11 features needs to change for C17.

C23 (published in 2024) is the cosmetic-but-welcome refresh. The headline is that several things which used to need a header are now plain keywords: bool, true, and false are built in, so you no longer include <stdbool.h>; static_assert and the alignment names became keywords too. C23 also borrows three good ideas from C++: a real nullptr constant for null pointers (clearer than the old NULL or a bare 0), the typeof operator to name "the type of this expression", and constexpr for true compile-time constants. Guide 5 returns to these.

The change with the biggest readability payoff is standard attributes: a uniform bracketed syntax for telling the compiler facts it cannot infer. Mark a function so the compiler warns if its result is ignored, or mark a switch case so it does not warn about an intentional fall-through. Before C23 these existed only as non-portable extensions like __attribute__((...)). Now they are spelled in a way every C23 compiler understands. We write the attribute name in words here; the bracketed C syntax appears in the code samples of guide 5.

What did NOT change: the abstract machine

Here is the deepest point, and the one that ties this whole rung to the earlier ones. None of these standards changed the C abstract machine — the imaginary computer the language is defined against. C does not promise to do what your real CPU does; it promises to behave as if your program ran on this idealized machine. The compiler may reorder, delete, or transform your code freely under the as-if rule, as long as the observable behavior of that abstract machine is preserved. That contract is unchanged from C89 to C23.

Why does that matter for a rung about new features? Because the abstract machine is also where undefined behavior lives, and modern C gives you better tools to stay inside its rules rather than weakening them. Recall the honest definition from the earlier rungs: undefined behavior is not "whatever the compiler feels like". It is a license the optimizer takes — it is allowed to assume UB never happens. That is exactly why a bug can be invisible at gcc -O0 and corrupt memory at gcc -O2: the optimizer reasoned about code paths assuming you never triggered UB. No new standard makes UB safe; what C11's _Static_assert and C23's attributes do is help you prevent the situations that lead there.