Standard Library Overview

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

C’s standard library is small by design — around 30 headers, no containers, no networking, no filesystem traversal. What it does provide is the portable floor every C program stands on, and a set of conventions that every C API since has imitated.

The Header Catalogue

C23 defines these headers. The ones marked C23 are new in this edition; the ones marked optional may be absent, and the corresponding __STDC_NO_*__ macro says so.

Header Provides

<assert.h>

assert, and C23’s static_assert macro alias.

<complex.h> (optional)

Complex arithmetic: creal, cimag, cabs, csqrt. __STDC_NO_COMPLEX__.

<ctype.h>

Character classification: isalpha, isdigit, isspace, tolower, toupper.

<errno.h>

errno and the E* error macros (EDOM, ERANGE, EILSEQ).

<fenv.h>

Floating-point environment: rounding modes and exception flags.

<float.h>

Floating-point limits: DBL_DIG, DBL_EPSILON, FLT_MAX.

<inttypes.h>

printf/scanf format macros (PRId64, SCNu32), imaxdiv, strtoimax.

<iso646.h>

Alternative spellings (and, or, not). Deprecated in C23.

<limits.h>

Integer limits: CHAR_BIT, INT_MAX, ULLONG_MAX, and C23’s BITINT_MAXWIDTH.

<locale.h>

setlocale, localeconv.

<math.h>

Real maths: sqrt, pow, fma, isnan, fpclassify.

<setjmp.h>

Non-local jumps: setjmp, longjmp.

<signal.h>

signal, raise, sig_atomic_t.

<stdalign.h>

C11 alignas/alignof macros. Deprecated in C23, where both are keywords.

<stdarg.h>

Variadic arguments: va_list, va_start, va_arg, va_end, va_copy.

<stdatomic.h> (optional)

Atomics: atomic_load, atomic_fetch_add, memory_order. __STDC_NO_ATOMICS__.

<stdbit.h> C23

Bit utilities: stdc_count_ones, stdc_bit_width, stdc_bit_ceil, endianness macros.

<stdbool.h>

C99 bool/true/false macros. Deprecated in C23, where all three are keywords.

<stdckdint.h> C23

Checked integer arithmetic: ckd_add, ckd_sub, ckd_mul.

<stddef.h>

size_t, ptrdiff_t, NULL, offsetof, max_align_t, C23’s nullptr_t and unreachable.

<stdint.h>

Fixed-width integers: int32_t, uintmax_t, INT64_MAX, SIZE_MAX.

<stdio.h>

Streams and files: printf, scanf, fopen, fread, fseek.

<stdlib.h>

Allocation, conversion (strtol), qsort, bsearch, abs, rand, exit, getenv.

<stdnoreturn.h>

C11 noreturn macro. Deprecated in C23 in favour of [[noreturn]].

<string.h>

Strings and memory: strlen, strcmp, memcpy, memmove, and C23’s memset_explicit.

<tgmath.h>

Type-generic maths macros over <math.h> and <complex.h>.

<threads.h> (optional)

Threads: thrd_create, mtx_t, cnd_t, tss_t. __STDC_NO_THREADS__.

<time.h>

Time: time_t, timespec_get, strftime, and C23’s timegm, gmtime_r.

<uchar.h>

Unicode: char8_t, char16_t, char32_t and their conversion functions.

<wchar.h>

Wide strings: wprintf, wcslen, mbrtowc.

<wctype.h>

Wide character classification: iswalpha, towupper.

C23 also removed things: the K&R-era gets (gone since C11), trigraphs, and the old __STDC_ISO_10646__-dependent behavior. Nothing that compiled cleanly with -Wall in C17 was broken by C23 apart from gets.

Interface Conventions

Almost every function in the library reports failure in one of four ways. Learning the pattern is more useful than memorizing individual signatures.

1. A Sentinel Return Value

malloc returns nullptr, fopen returns nullptr, fgets returns nullptr, getchar returns EOF, strchr returns nullptr. Check the return, every time.

2. A Return Code

fclose, fseek, remove, rename, raise and the <threads.h> functions return 0/non-zero or a named status (thrd_success).

3. errno

A global (in practice thread-local) error number set by library functions on failure. Its discipline is specific and widely got wrong:

#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    // Rule 1: errno is only meaningful after a function DOCUMENTED to set it fails.
    //         A successful call may still change it.
    FILE *f = fopen("/nonexistent/path", "r");
    if (f == nullptr) {
        // Rule 2: read errno immediately -- any intervening call may overwrite it.
        int saved = errno;
        fprintf(stderr, "fopen failed: %s\n", strerror(saved));
        perror("fopen");                    // the same message, prefixed, to stderr
    }

    // Rule 3: for functions that return a valid value on failure (strtol, the math
    //         functions), you must clear errno to zero BEFORE the call.
    errno = 0;
    char *end = nullptr;
    long value = strtol("99999999999999999999", &end, 10);
    if (errno == ERANGE) {
        printf("out of range, clamped to %ld\n", value);
    }

    errno = 0;
    double r = log(-1.0);
    if (errno == EDOM) {
        printf("log(-1) is a domain error, returned %g\n", r);
    }

    return 0;
}

Only three errno values are defined by C itself — EDOM, ERANGE and EILSEQ; everything else (ENOENT, EACCES, …) comes from POSIX. strerror is not thread-safe in principle; strerror_r (POSIX) or C23’s strerror with a locale-independent guarantee is the safer choice in threaded code.

4. An Out Parameter

strtol writes the parse end position through char **end; timespec_get fills a struct timespec; thrd_create writes the thread handle. The return value then carries only success/failure.

Annex K — The Bounds-Checking Interfaces

C11 added an optional Annex K: strcpy_s, sprintf_s, fopen_s and friends, which take destination sizes and call a runtime constraint handler on violation.

#include <stdio.h>

int main(void)
{
    // Annex K is optional, and this is how you detect it:
#if defined(__STDC_LIB_EXT1__)
    puts("Annex K bounds-checking interfaces are available");
#else
    puts("no Annex K -- use snprintf and explicit sizes");
#endif
    return 0;
}

The practical position: Annex K is implemented essentially only by MSVC. glibc, musl, the BSD libcs and Apple’s libc all decline it, and WG14’s own N1969 report recommended against it. Do not build a portable codebase on it. Use snprintf, explicit sizes, and -D_FORTIFY_SOURCE=3 instead — see Strings and Text Processing.

Feature-Test Macros

How to ask what the implementation actually supports:

#include <stdio.h>

int main(void)
{
    printf("__STDC__          = %d\n", __STDC__);           // 1 for a conforming impl
    printf("__STDC_VERSION__  = %ld\n", __STDC_VERSION__);  // 202311L for C23
    printf("__STDC_HOSTED__   = %d\n", __STDC_HOSTED__);    // 1 hosted, 0 freestanding

#ifdef __STDC_NO_THREADS__
    puts("no <threads.h>");
#endif
#ifdef __STDC_NO_ATOMICS__
    puts("no <stdatomic.h>");
#endif
#ifdef __STDC_NO_COMPLEX__
    puts("no <complex.h>");
#endif
#ifdef __STDC_NO_VLA__
    puts("no variable-length arrays");
#endif
#ifdef __STDC_IEC_60559_BFP__
    puts("IEEE-754 binary floating point");
#endif
#ifdef __STDC_UTF_8__
    puts("char8_t literals are UTF-8");
#endif

    return 0;
}

Note the distinction between a hosted and a freestanding implementation: a freestanding one (a kernel, an MCU toolchain with -ffreestanding) is only required to provide <float.h>, <limits.h>, <stdarg.h>, <stdbit.h>, <stdalign.h>, <stdbool.h>, <stddef.h>, <stdint.h>, <stdnoreturn.h> — no printf, no malloc, and main need not be the entry point.

Assertions

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

static int divide(int numerator, int denominator)
{
    // A programming-error check: this must never fire in a correct program.
    assert(denominator != 0 && "denominator must not be zero");
    return numerator / denominator;
}

static int parse_port(const char *text)
{
    // An INPUT check is not an assertion -- it must survive -DNDEBUG.
    if (text == nullptr) {
        return -1;
    }
    long value = strtol(text, nullptr, 10);
    if (value < 1 || value > 65535) {
        return -1;
    }
    return (int)value;
}

int main(void)
{
    printf("%d %d %d\n", divide(10, 2), parse_port("8080"), parse_port("99999"));
    return 0;
}

The line to hold onto: assert is compiled out entirely when NDEBUG is defined (-DNDEBUG, which release builds normally set), including its side effects. Never put a required operation inside an assert, and never use assert to validate external input. Use it for invariants and preconditions your own code must uphold.

The && "message" idiom works because a string literal is always non-null, so it prints the message with the failed expression.

C23 also makes static_assert a keyword for compile-time checks — see Constants, Enumerations and Initialization.

Program Termination

Function Behavior

return from main

Destroys main’s locals, then behaves as `exit with that value.

exit(status)

Runs atexit handlers in reverse registration order, flushes and closes all streams, removes `tmpfile`s, then terminates. Does not return.

quick_exit(status)

Runs at_quick_exit handlers; does not flush streams. C11.

_Exit(status)

Terminates immediately: no handlers, no flushing.

abort()

Raises SIGABRT; typically dumps core. No handlers, no flushing (unless a SIGABRT handler intervenes).

#include <stdio.h>
#include <stdlib.h>

static void flush_cache(void)
{
    puts("2. flush_cache (registered last, runs first)");
}

static void close_log(void)
{
    puts("3. close_log (registered first, runs last)");
}

int main(void)
{
    if (atexit(close_log) != 0 || atexit(flush_cache) != 0) {
        return EXIT_FAILURE;                // registration can fail
    }

    puts("1. work finished");
    exit(EXIT_SUCCESS);                     // handlers run in reverse order
}

At least 32 atexit handlers must be supported. A handler must not call exit again (undefined), and after exit begins, calling longjmp out of a handler is undefined too.

The Environment

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    const char *home = getenv("HOME");
    const char *missing = getenv("DEFINITELY_NOT_SET_12345");

    printf("HOME=%s missing=%s\n",
           home != nullptr ? home : "(unset)",
           missing != nullptr ? missing : "(unset)");

    // system(nullptr) asks whether a command processor exists at all.
    if (system(nullptr) != 0) {
        puts("a command processor is available");
    }
    return 0;
}

getenv returns a pointer to a string you must not modify or free, and which a later getenv or setenv may invalidate — copy it if you need to keep it. There is no standard setenv (that is POSIX).

system runs a command through the shell, which makes it a command-injection hazard with any untrusted input, and it is not thread-safe. Prefer POSIX posix_spawn/fork+execve with an argument array, which never involves a shell.

See Also