Site icon ucdev

History of the C Programming Language – From K&R to C23

The C programming language was created more than 50 years ago, and it remains one of the most widely used languages today, particularly for embedded systems, operating systems and low-level software development.

Since its creation in 1972, the C programming language has evolved significantly. Several standards have been introduced over the years to improve portability, extend the language’s capabilities and address the changing needs of software developers.

In this article, we will explore the history of the C programming language, from its early days at Bell Labs to the most recent C23 standard. We will also examine the most significant features introduced by each version, and consider their influence on modern C programming.

The Origins of C (1972–1989)

The C programming language was developed in the early 1970s by Dennis Ritchie at Bell Laboratories.

It evolved from an earlier programming language called B, which itself was derived from BCPL. C was primarily designed to support the development of the Unix operating system.

One of the most important milestones occurred in 1973, when much of the Unix kernel was rewritten in C. This demonstrated that a high-level programming language could be used effectively for system-level programming, which had previously been dominated by assembly language.

K&R C – Before Standardization

In 1978, Dennis Ritchie and Brian Kernighan published The C Programming Language, commonly known as the K&R book. For many years, this book served as the primary reference for C programmers. The language described in it is often referred to as K&R C.

At that time, there was no official ANSI or ISO standard defining the language. Different compilers could implement certain features differently, making it difficult to write fully portable code. For many years, this book served as the primary reference for C programmers. The language described in it is often referred to as K&R C.

As C became increasingly popular, the need for a formal language specification became clear.

C Programming Language Standards – Timeline

Over the years, several C standards have been published. Each revision aimed to standardize existing practices, introduce new features, or improve the language’s consistency.

YearStandardKey Changes
1972Original CCreation of the C programming language
1978K&R CPublication of The C Programming Language
1989C89First official ANSI standard
1990C90International ISO standardization
1995C95Improvements to character handling
1999C99New data types, VLAs, inline, restrict
2011C11Atomic operations, multithreading, _Generic
2018C17Bug fixes and clarifications
2024C23New keywords and modern language features

Now, let’s take a closer look at the most significant changes introduced by each standard.

C89 and C90 – The First Official C Standards

The first official C standard was published in 1989 by the American National Standards Institute (ANSI). One year later, it was adopted by the International Organization for Standardization (ISO) as ISO/IEC 9899:1990. These standards are commonly referred to as C89 and C90.

Although they have different names, they define essentially the same language. The primary goal was to establish a consistent specification that compiler developers could follow, improving code portability across different systems.

Key Features of C89/90

The standard formalized several important language features, including:

These features were not necessarily invented in C89. Many had already appeared in compiler implementations before standardization.

C89 also standardized function prototypes, allowing compilers to check argument types during function calls.

From an embedded development perspective, the volatile qualifier is particularly important because it is commonly used when accessing memory-mapped hardware registers.

C95 – An Update to C90

In 1995, an amendment to C90 was published, commonly referred to as C95. Rather than introducing a completely new language version, C95 focused primarily on improving international character support.

It introduced additional facilities for wide and multibyte characters, including the <wchar.h> and <wctype.h> headers.

C99 – A Major Evolution of C

In 1999, the ISO published ISO/IEC 9899:1999, commonly known as C99. This was the first major revision of the C language since its initial standardization. C99 introduced numerous features that are now considered fundamental parts of modern C programming.

New Data Types and Keywords

Among the most important additions were:

C99 also extended the standard library with several new headers. Two particularly useful additions were:

<stdint.h>

Introduced fixed-width integer types, such as:

uint8_t
uint16_t
uint32_t
int64_t

These types are particularly useful in embedded systems, where developers frequently work with hardware registers, communication protocols, and data structures requiring specific integer widths.

<stdbool.h>

Introduced convenient Boolean macros:

bool
true
false

Variable Length Arrays (VLA)

C99 introduced Variable Length Arrays, allowing the size of an array to be determined at runtime. For example:

void process_data(int size)
{
    int buffer[size];
}

Unlike traditional fixed-size arrays, the size of buffer does not need to be a compile-time constant. However, VLAs should be used carefully in embedded systems because they can consume unpredictable amounts of stack memory. Support for automatic VLAs became optional in C11.

Other Important C99 Features

C99 also introduced several improvements to the language syntax:

One particularly useful feature is designated initialization. Consider the following structure:

struct Device {
    int id;
    int status;
};

Before C99, initialization typically looked like this:

struct Device device = {10, 1};

With C99, we can explicitly specify the members:

struct Device device = {
    .id = 10,
    .status = 1
};

This improves readability and makes the code less dependent on the order of structure members. Designated initializers are widely used in embedded software, particularly when defining hardware configurations and device structures.

C11 – Atomic Operations and Multithreading

The next major revision arrived in 2011 with ISO/IEC 9899:2011, commonly known as C11. One of its main goals was to provide standardized mechanisms for concurrent programming.

As multicore processors became increasingly common, the language needed better support for communication and synchronization between threads.

Atomic Operations – _Atomic

C11 introduced atomic types and operations through the <stdatomic.h> header. Atomic operations allow certain operations on shared data to be performed without interference from concurrent accesses. For example:

#include <stdatomic.h>

atomic_int counter = 0;

void increment(void)
{
    atomic_fetch_add(&counter, 1);  // Incrementing counter is performed atomically
}

This is particularly useful when multiple threads need to modify shared variables. However, atomic operations do not automatically make an entire program thread-safe. Proper synchronization is still required when multiple related operations must be coordinated.

Thread Support – _Thread_local

C11 also introduced the _Thread_local storage-class specifier. C11 also introduced the _Thread_local storage-class specifier. Additionally, the standard introduced the <threads.h> library, which provides facilities for:

It is worth mentioning that support for the standard C threading library is not available in every implementation, particularly in embedded environments. Embedded applications often use the threading mechanisms provided by their operating system, such as an RTOS, instead.

Type-Generic Expressions – _Generic

Another interesting feature introduced in C11 was _Generic. It allows an expression to be selected based on the type of its argument. For example:

#define get_type(x) _Generic((x), \
    int: "integer",               \
    float: "float",               \
    double: "double",             \
    default: "unknown")

This mechanism makes it possible to create type-dependent interfaces without relying on C++ features such as function overloading.

Compile-Time Assertions – _Static_assert

C11 introduced _Static_assert, allowing developers to verify conditions during compilation. For example:

#include <stdint.h>

_Static_assert(sizeof(uint32_t) == 4,
               "Invalid uint32_t size");

If the condition is false, compilation fails. This is especially useful in embedded systems, where data sizes, memory layouts, and hardware-related assumptions must often be verified at compile time.

Other Important C11 Features

C11 introduced several additional improvements:

C17 – Bug Fixes and Clarifications

In 2018, another revision of the C standard was published as ISO/IEC 9899:2018, commonly known as C17. Unlike C99 and C11, C17 did not introduce significant new language features. Unlike C99 and C11, C17 did not introduce significant new language features. The main changes included:

From a developer’s perspective, the differences between C11 and C17 are relatively small. Consequently, code written for C11 will generally also compile under C17 without modification.

C23 – The Latest C Programming Standard

The latest published C language standard is C23, formally published as ISO/IEC 9899:2024. Although its name refers to 2023, the official ISO publication took place in 2024.

C23 introduces several modern language features, improves existing functionality, and makes some commonly used compiler extensions part of the standardized language.

Some of the most notable changes include:

For example, binary literals can now be written as:

unsigned int mask = 0b10101010;

This syntax can be particularly convenient in embedded programming, where binary representations are frequently used for bit masks and register configurations. Another interesting addition is #embed, which allows binary data to be included directly in a program. This could be useful when embedding configuration data, images, or other binary resources into firmware.

As with any new language standard, support for C23 features depends on the compiler and its version.

How to Check the C Standard Used by Your Compiler

The C programming language provides a predefined macro called __STDC_VERSION__, which can be used to identify the language standard selected during compilation. The macro expands to a numeric value corresponding to a particular standard.

Standard__STDC_VERSION__
C89/C90Not defined
C95199409L
C99199901L
C11201112L
C17201710L
C23202311L

Note that __STDC_VERSION__ is not defined in the original C89/C90 standard. We can use this macro to conditionally compile code depending on the selected C standard.

For example:

#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
    #define C_STANDARD "C23"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201710L
    #define C_STANDARD "C17"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
    #define C_STANDARD "C11"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
    #define C_STANDARD "C99"
#else
    #define C_STANDARD "C90 or older"
#endif

Keep in mind that this macro identifies the selected language standard, but it does not guarantee support for every optional feature.

Selecting the C Standard in GCC

When using the GCC compiler, we can explicitly select the C standard with the -std option. For example, to compile a program using C99:

gcc -std=c99 main.c -o main

Similarly, we can select other standards:

gcc -std=c11 main.c -o main
gcc -std=c17 main.c -o main
gcc -std=c23 main.c -o main

The -std=c23 option requires a sufficiently recent GCC version. GCC also provides GNU dialects of the standards, such as gnu11 and gnu17. These modes enable additional GNU-specific extensions alongside the selected C standard.

Which C Standard Is Used in Embedded Systems?

Despite the introduction of newer standards, older versions of C are still widely used in embedded software development. The choice of standard often depends on several factors:

Historically, C90 and C99 have been particularly common in embedded development, often combined with compiler-specific extensions.

However, modern embedded projects also make use of C11 and C17, depending on their toolchains and requirements.

Using the latest available C standard is not always necessary. In embedded development, choosing a stable and well-supported toolchain is often more important than adopting every new language feature.

Summary

The history of the C programming language demonstrates how a language can evolve over more than five decades while preserving its original design principles.

From its early development at Bell Labs to the latest C23 standard, C has continuously adapted to the changing requirements of software development.

Some of the most significant improvements were introduced in C99 and C11, which added features such as fixed-width integer types, designated initializers, atomic operations, and standardized support for concurrent programming.

Although newer programming languages have emerged, C remains a fundamental tool for low-level software development, particularly in embedded systems, where efficiency, predictable behavior, and direct hardware access are essential.

Understanding the evolution of C standards helps developers make better decisions about language features, compiler compatibility, and portability when working on modern embedded projects.

Exit mobile version