C Standards: K&R to C23

C · Lesson 31 of 32 8 min read Updated

C has been in use for over fifty years, and the language has changed during that time. The changes are recorded in standards: official documents that define exactly what valid C code means, so that a program written for one compiler works with another. Knowing the versions helps you read old code, follow textbooks and exam questions written for older C, and choose the right compiler options.

In this lesson you will walk through every major version of C, from the original K&R book to C23, see what each one added, and learn how to tell your compiler which standard to use.

Who makes the C standard?

The first C standard was published by ANSI, the American National Standards Institute, in 1989. Since then, C has been standardised internationally by ISO and IEC. The work is done by a committee called WG14 (formally ISO/IEC JTC1/SC22/WG14), made up of compiler writers, library authors and users. The committee's working papers are public on open-std.org.

The timeline

VersionYearOfficial document__STDC_VERSION__
K&R C1978The book The C Programming LanguageNot defined
C89 / C901989 / 1990ANSI X3.159-1989, then ISO/IEC 9899:1990Not defined
C951995Amendment 1 to C90199409L
C991999ISO/IEC 9899:1999199901L
C112011ISO/IEC 9899:2011201112L
C172018ISO/IEC 9899:2018201710L
C232024ISO/IEC 9899:2024202311L

The nicknames do not always match the publication years: C17 was published in 2018 (and is sometimes called C18), and C23 was published on 31 October 2024. The version macro uses the year and month in which the text was finalised.

K&R C (1978)

Dennis Ritchie created C at Bell Labs in the early 1970s. In 1978, Brian Kernighan and Dennis Ritchie published The C Programming Language, and for about ten years that book, known as K&R, was the only definition of C. K&R C had no function prototypes: the compiler did not check the number or types of arguments. Functions defined their parameters in a separate list, and a missing type meant int:

C
/* K&R style (1978): not valid in C23 */
main()
{
    int total;
    total = add(40, 2);
    printf("%d\n", total);
}

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

C89 and C90: the first standard

ANSI standardised C in 1989 (C89, also called ANSI C), and ISO adopted the same text in 1990 (C90), so C89 and C90 are the same language. It added function prototypes (borrowed from C++), the keywords void, const, volatile and signed, and, very importantly, a standard library with headers such as stdio.h, stdlib.h and string.h. The second edition of the K&R book (1988) described this ANSI C. A small amendment in 1995 (C95) added better support for wide characters and international text.

C99: the big modernisation

C99 brought many features that modern C code relies on:

  • // comments, and declarations anywhere in a block, including inside for (int i = 0; ...).
  • long long, the _Bool type with stdbool.h, and fixed-width types in stdint.h and inttypes.h.
  • inline functions, snprintf, restrict pointers and the __func__ name.
  • Designated initialisers (.roll = 15, [2] = 50) and compound literals.
  • Variable-length arrays, flexible array members, variadic macros and complex.h.

C11: concurrency and safety

C11 added _Generic for type-generic code, _Static_assert for compile-time checks, anonymous structures and unions, _Alignas and _Alignof, _Noreturn, Unicode types char16_t and char32_t in uchar.h, and aligned_alloc. For programs that run several threads at once it added threads.h and atomics in stdatomic.h, both optional. It removed gets, made variable-length arrays optional, added the "x" mode for fopen, and introduced the optional bounds-checking "Annex K" functions such as strcpy_s, which few libraries implement.

C
#include <stdio.h>

#define type_name(x) _Generic((x), int: "int", double: "double", char *: "string", default: "other")

_Static_assert(sizeof(int) >= 4, "int must be at least 32 bits");

int main(void)
{
    printf("%s, %s, %s\n", type_name(42), type_name(3.5), type_name("Pune"));
    return 0;
}
Output
int, double, string

_Generic chooses a result based on the type of its first argument, at compile time. _Static_assert stops compilation with the message if its condition is false; here it passes on every common system. The underscore-and-capital spellings were chosen so that the new keywords could not clash with names in existing programs.

C17: bug fixes

C17 added no new features. It corrected defects and unclear wording found in C11. For practical purposes, C11 and C17 are the same language, and C17 was the default in GCC for several years.

C23: the current standard

C23 is the largest update since C99. Many of its features have appeared throughout this course:

  • New keywords: bool, true and false (Data Types & bool); nullptr (Pointers, NULL & nullptr); constexpr (Constants); auto type inference; typeof and typeof_unqual; and the plain spellings static_assert, alignas, alignof and thread_local.
  • Syntax: standard attributes such as [[nodiscard]], [[maybe_unused]], [[deprecated]] and [[fallthrough]]; binary literals (0b1010); digit separators (1'00'000); empty initialisers = {}; enums with a fixed underlying type; and bit-precise integers _BitInt(N).
  • Preprocessor: #embed, #elifdef, #elifndef, #warning and __has_include.
  • Library: checked arithmetic in stdckdint.h, bit utilities in stdbit.h, strdup and strndup, memset_explicit for wiping secrets, unreachable(), and the %b binary format for printf.
  • Removals and clean-ups: K&R-style function definitions are gone, int f() now means "no parameters", and signed integers must use two's complement.

Here is a small program written in C23 style:

C
#include <stdio.h>

constexpr int SEATS = 8;

[[nodiscard]] int count_free(const bool booked[], int n)
{
    auto count = 0;
    for (int i = 0; i < n; i++) {
        if (!booked[i]) {
            count++;
        }
    }
    return count;
}

int main(void)
{
    bool booked[SEATS] = {};
    booked[2] = true;
    booked[5] = true;

    const char *note = nullptr;
    int free_seats = count_free(booked, SEATS);
    if (free_seats < SEATS) {
        note = "some seats are taken";
    }
    printf("Free seats: %d of %d (%s)\n", free_seats, SEATS, note);
    return 0;
}
Output
Free seats: 6 of 8 (some seats are taken)

The same program in C17 would need #include <stdbool.h> for bool, #define SEATS 8 instead of constexpr, = {0} instead of = {}, int count instead of auto, NULL (from stddef.h) instead of nullptr, and it would have no [[nodiscard]].

Choosing a standard with -std

GCC and Clang let you pick the standard with -std. The gnu variants (such as -std=gnu17 or -std=gnu23) add the compiler's own extensions; the plain c variants follow the standard more strictly, especially together with -pedantic, which warns when you use an extension.

OptionStandard
-std=c89 or -std=c90C89/C90
-std=c99C99
-std=c11C11
-std=c17C17
-std=c23C23 (GCC 14 and newer; GCC 13 uses -std=c2x)

GCC 15 and newer use gnu23 by default; other compilers and older versions may default to an older standard, so pass -std explicitly. If you must build code for an older compiler, choose the newest standard it supports and avoid C23-only features.

Common mistakes

  • Learning from very old material without knowing it. void main(), gets, K&R definitions and implicit int belong to the past.
  • Mixing up C and C++ standards. C23 and C++23 are different documents with different rules; C has no classes, templates or constexpr functions.
  • Relying on the compiler's default standard. It differs between compiler versions; always pass -std.
  • Assuming every C23 feature exists everywhere. Library additions such as stdbit.h depend on your C library version, not only the compiler.

Key points

  • K&R C (1978) was defined by a book; C89/C90 was the first official standard and added prototypes and the standard library.
  • C99 added // comments, long long, stdbool.h, stdint.h, designated initialisers and declarations anywhere.
  • C11 added _Generic, _Static_assert, threads and atomics and removed gets; C17 only fixed defects.
  • C23 (ISO/IEC 9899:2024) added bool, nullptr, constexpr, auto, typeof, attributes, #embed and checked arithmetic, and removed K&R definitions.
  • __STDC_VERSION__ identifies the standard: 201710L for C17 and 202311L for C23.
  • Select the standard with -std=c23 (or another version) and add -pedantic to catch extensions.

Exercise

Rewrite this old-style program in modern C23. Use a proper prototype-style definition, int main(void), the bool type instead of TRUE and FALSE macros, and declare the loop variable inside the for.

C
#include <stdio.h>
#define TRUE 1
#define FALSE 0

int is_even();

main()
{
    int i;
    int count;
    count = 0;
    for (i = 1; i <= 10; i++) {
        if (is_even(i) == TRUE) {
            count++;
        }
    }
    printf("Even numbers: %d\n", count);
    return 0;
}

int is_even(n)
int n;
{
    if (n % 2 == 0) return TRUE;
    return FALSE;
}
Show solution
C
#include <stdio.h>

bool is_even(int n)
{
    return n % 2 == 0;
}

int main(void)
{
    int count = 0;
    for (int i = 1; i <= 10; i++) {
        if (is_even(i)) {
            count++;
        }
    }
    printf("Even numbers: %d\n", count);
    return 0;
}
Output
Even numbers: 5

is_even now has a typed parameter list, so the compiler checks every call. It returns the comparison directly as a bool, and the if tests it without comparing to TRUE. Defining is_even before main removes the need for a separate prototype.

Quick Quiz

1. Which standard first added function prototypes and the standard library?

2. Which version removed the gets function?

3. What is special about C17?

4. Which value of __STDC_VERSION__ means C23?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call