../

C fundamentals

Core C23 (ISO/IEC 9899:2024, with notes where C17 and C11 differ) for someone who already writes Python or TypeScript: the toolchain, types and conversions, the preprocessor, programs split across files, strings, structs, I/O, error handling, the undefined behavior that bites, and building with make. Pointers and the heap have their own sheet, C memory management & pointers; C++, which grew out of C, is in C++ fundamentals.

Why C and where it runs

C is a small, statically typed, compiled language created by Dennis Ritchie at Bell Labs in the early 1970s to rewrite Unix. A compiler turns source code into native machine code for one CPU and operating system. There is no virtual machine, no garbage collector, no exceptions and no bounds checking: the language trusts you, which is why it is fast, runs on almost any chip, and is easy to get wrong.

WhereExamples
Operating systemsthe Linux and BSD kernels, large parts of Windows and macOS
Embedded and firmwaremicrocontrollers (Arm Cortex-M, AVR, ESP32, RISC-V), where C is often the vendor's main supported language
Language runtimesCPython, Lua, PHP, Ruby's reference interpreter
InfrastructureSQLite, PostgreSQL, Redis, Git, curl, nginx
Glue between languagesthe C ABI (how compiled functions call each other) is the common ground: Python's ctypes, Node-API, Rust's extern "C" and Go's cgo all call C functions
StandardNameWhat it added
ANSI 1989, ISO 1990C89 / C90function prototypes, const, void *, the standard library
ISO/IEC 9899:1999C99// comments, declarations anywhere, <stdint.h>, _Bool, long long, designated initializers, compound literals, variable-length arrays (VLAs), inline
ISO/IEC 9899:2011C11_Generic, _Static_assert, _Alignas, anonymous structs and unions, optional <threads.h> and <stdatomic.h>; gets removed, VLAs made optional
ISO/IEC 9899:2018C17defect fixes only
ISO/IEC 9899:2024C23nullptr, bool/true/false keywords, typeof, auto, constexpr, [[attributes]], #embed, digit separators: see C23 additions

Compiler support: GCC 15 compiles C as -std=gnu23 (C23 plus GNU extensions) by default; older GCC releases default to an older dialect (gnu17 in recent ones). Clang accepts -std=c23 from version 18 (older compilers spell it -std=c2x), and some features need newer releases: constexpr and #embed arrived in Clang 19. MSVC (Visual Studio) supports C11 and C17 with /std:c11 and /std:c17 and parts of C23 with /std:clatest; its flags differ (/W4, /fsanitize=address) but the language is the same.

Toolchain

gcc main.c runs four programs in a row. Knowing which one complains tells you where to look.

preprocess compile assemble link main.c main.i main.s main.o source expanded C assembly object -E -S -c util.c util.o same three steps: gcc -c util.c libc libraries: -lm, -lname ld app binary preprocess: paste in #include files, expand macros → one translation unit compile, assemble: each .c on its own → .o with unresolved symbols link: give every symbol exactly one definition, add libraries → executable
Each .c file is compiled on its own; only the linker sees the whole program.
StageToolInput → outputErrors from this stage
preprocesscpp (gcc -E).c plus its headers → one translation unit (TU)missing header, broken macro
compilecc1 (gcc -S)C → assembly (.s)syntax and type errors, all warnings
assembleas (gcc -c)assembly → object file (.o): machine code plus a symbol tablealmost never
linkld (driven by gcc).o files + libraries → executableundefined reference, multiple definition

Clang runs the first three stages in one process but accepts the same flags.

# Debug build: warnings, symbols and runtime checks
gcc -std=c23 -Wall -Wextra -pedantic -g -O0 \
    -fsanitize=address,undefined main.c -o main
./main
echo "exit status: $?"
 
# Release build
clang -std=c23 -Wall -Wextra -O2 -DNDEBUG main.c -o main
 
# Stop after one stage to look at its output
gcc -E main.c -o main.i      # preprocessed C
gcc -S main.c                # main.s: assembly
gcc -c main.c                # main.o: object file
gcc main.o util.o -o app     # link two objects
nm main.o                    # symbols: T defined, U needed
FlagMeaning
-std=c23language version: c17, c11, c99; gnu23 also allows GNU extensions
-Wall -Wextrathe common warnings (-Wall is not "all"); always on
-pedanticwarn about anything that is not ISO C for the chosen -std
-Werrorturn warnings into errors: use in CI
-Wconversion, -Wshadowstricter extras: implicit narrowing, shadowed names
-O0 / -O2 / -O3 / -Osoptimization: none (default), standard, aggressive, small
-gdebug info for gdb and lldb
-fsanitize=address,undefinedAddressSanitizer + UndefinedBehaviorSanitizer: runtime checks, about 2× slower
-I diradd a directory to the header search path (#include "x.h")
-L diradd a directory to the library search path
-l namelink libname.so or libname.a, e.g. -lm for <math.h> on Linux; put it after the files that use it
-ccompile to an object file, do not link
-o filename the output (default a.out)
-D NAME=valuedefine a macro; -DNDEBUG turns assert off
-MMD -MPwrite .d dependency files for make

Reading compiler errors

This file has three mistakes on purpose:

oops.c
#include <stdio.h>
 
int main(void) {
    int count = 3
    printf("%s items\n", count);
    return total;
}
$ gcc -Wall -Wextra oops.c
oops.c: In function 'main':
oops.c:5:5: error: expected ',' or ';' before 'printf'
    5 |     printf("%s items\n", count);
      |     ^~~~~~
oops.c:6:12: error: 'total' undeclared (first use in this function)
    6 |     return total;
      |            ^~~~~
oops.c:4:9: warning: unused variable 'count' [-Wunused-variable]

The format is file:line:column: severity: message. The missing ; is on line 4 but reported on line 5, and the "unused variable" warning is a side effect of the first error. Fix the first error, recompile, repeat. Once it compiles, GCC also reports the third mistake: format '%s' expects argument of type 'char *', but argument 2 has type 'int'.

MessageUsually means
expected ';' before 'x'the ; is missing at the end of the previous line
'x' undeclareda typo, or a name declared in another scope or file
implicit declaration of function 'f'missing #include or prototype; an error by default in GCC 14+ and Clang 16+
incompatible pointer typee.g. int * passed where long * is expected; an error by default in GCC 14+
format '%d' expects argument of type 'int'the printf conversion does not match the argument
control reaches end of non-void functiona path with no return value
undefined reference to 'f'a linker error: f is declared but never defined; a .c file or -l library is missing
multiple definition of 'x'a linker error: a variable or function is defined in a header included by two .c files

Program structure

hello.c
#include <stdio.h>   // printf, fprintf
#include <stdlib.h>  // EXIT_SUCCESS, EXIT_FAILURE
 
int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "usage: %s NAME...\n", argv[0]);
        return EXIT_FAILURE;  // nonzero means failure
    }
    for (int i = 1; i < argc; i++)
        printf("hello, %s\n", argv[i]);
    return EXIT_SUCCESS;      // 0
}
cc -std=c23 -Wall -Wextra hello.c -o hello
./hello Ada Linus    # hello, Ada / hello, Linus
./hello; echo $?     # usage message, then 1
PieceMeaning
#include <stdio.h>paste a header's declarations in; <...> for system headers, "..." for your own
int main(void)entry point without arguments; in C23 int main() means the same
int main(int argc, char *argv[])argc counts arguments including the program name in argv[0]; argv[argc] is a null pointer
return n; from mainsame as exit(n): flushes output, runs atexit handlers; the shell sees n & 0xFF on POSIX
no return in mainreturns 0 (only main gets this)
exit(n) / abort()end the program from anywhere; abort skips cleanup and raises SIGABRT
stdin, stdout, stderrthe three standard streams; diagnostics go to stderr
// and /* */comments; /* */ does not nest

Types

Integers

TypeStandard minimumx86-64 Linux, macOS64-bit Windowsprintf
char8 bits (CHAR_BIT)88%c, %hhd
short161616%hd
int163232%d
long326432%ld
long long646464%lld
size_tunsigned, 166464%zu
ptrdiff_tsigned6464%td

Each has an unsigned twin (unsigned int, unsigned long, …) with the same size; int is short for signed int. size_t is the type of sizeof and of every size and index in the library; ptrdiff_t is what subtracting two pointers gives. sizeof measures in bytes, and sizeof(char) is 1 by definition.

sizeof on x86-64 Linux and macOS (LP64) one cell = 1 byte = 8 bits bytes sizeof signed range unsigned max char 1 −128 … 127 255 short 2 −32768 … 32767 65535 int 4 −2147483648 … 2147483647 4294967295 long 8 ≈ −9.2e18 … 9.2e18 ≈ 1.8e19 long long 8 ≈ −9.2e18 … 9.2e18 ≈ 1.8e19 size_t 8 signed twin: ptrdiff_t ≈ 1.8e19 T * 8 any pointer: an address red: 64-bit Windows (LLP64) keeps long at 4 bytes; use int64_t for 8 exact limits: INT_MAX, LONG_MAX, SIZE_MAX … in <limits.h> and <stdint.h>
LP64: long and pointers are 8 bytes. 64-bit Windows keeps long at 4, so use int64_t when you need 64 bits.
<stdint.h>Meaning
int8_t, int16_t, int32_t, int64_texactly that width, two's complement; uint8_t … uint64_t unsigned
int_least16_t, int_fast32_tat least that wide / the fastest type at least that wide
intmax_t, uintmax_tthe widest integer types (%jd, %ju)
intptr_t, uintptr_tan integer that can hold a pointer
INT32_MAX, UINT64_MAX, SIZE_MAXlimits (<limits.h> has INT_MAX, LONG_MIN, …)
INT64_C(1)a constant of the right type

Use int for ordinary counters, size_t for sizes and indexes, and the fixed-width types for file formats, protocols, hashing and hardware registers. Print fixed-width types with the <inttypes.h> macros:

#include <inttypes.h>   // PRId64 etc.; includes <stdint.h>
#include <limits.h>
#include <stdio.h>
 
int main(void) {
    printf("int is %zu bytes: %d..%d\n", sizeof(int),
           INT_MIN, INT_MAX);
    int64_t big = INT64_MAX;
    uint8_t byte = 255;
    printf("%" PRId64 " %" PRIu8 "\n", big, byte);
}

Floating point, bool and char

TypeIn practiceprintf / scanfLiteral
floatIEEE 754 single, ≈7 significant digits%f / %f1.5f
doubleIEEE 754 double, ≈15–16 digits: the default%f, %g, %e / %lf1.5, 1e-9
long double80-bit extended on x86-64 Linux, same as double on MSVC and Apple Arm%Lf1.5L
bool0 or 1; C23 keyword (C99–C17: _Bool, or bool from <stdbool.h>)%dtrue, false
charone byte; signedness is implementation-defined: signed on x86, unsigned on Arm Linux%c'a'
  • 0.1 + 0.2 == 0.3 is false, as in every language with binary floating point. Compare with a tolerance.
  • <math.h> (sqrt, pow, floor, isnan, INFINITY) needs -lm when linking with glibc.
  • Floating-point division by zero gives inf or nan; integer division by zero is undefined behavior.
  • Any scalar converts to bool: zero is false, everything else (including 0.5 and any non-null pointer) is true. But comparisons and !, &&, || give an int 0 or 1, not a bool.
  • char, signed char and unsigned char are three distinct types. Use unsigned char (or uint8_t) for raw bytes and plain char only for text.

Conversions and overflow

Arithmetic in C never happens on types smaller than int. Integer promotion converts char, short, bool and bit-fields to int (or unsigned int if int cannot hold every value) before any operator runs. Then, for a binary operator such as + or <, the usual arithmetic conversions pick one common type:

  1. If either operand is floating, the other converts to the wider floating type.
  2. Otherwise, after promotion, the operand of lower rank converts to the type of higher rank (int < long < long long).
  3. When one is signed and one unsigned of the same rank (or the unsigned is wider), the signed one converts to unsigned. That is where most surprises come from.
ExpressionResultWhy
7 / 23integer division truncates
-7 / 2, -7 % 2-3, -1truncates toward zero; Python gives -4 and 1
7 / 2.03.5one double operand makes it floating
-1 < 0u0 (false)-1 converts to unsigned: 4294967295
(unsigned char)200 + (unsigned char)100300both promote to int first
uint8_t b = 200 + 100;44converting to an unsigned type wraps modulo 2ⁿ (256)
UINT_MAX + 1u0unsigned arithmetic wraps, always
INT_MAX + 1undefined behaviorsigned overflow is UB, not wrap-around
1 << 31undefined behavior (32-bit int)the result does not fit; write 1u << 31
(int)3.99, (int)-3.993, -3floating → integer truncates; out of range is UB
(int8_t)200-56 with GCC and Clangout-of-range conversion to a signed type is implementation-defined
strlen("") - 1SIZE_MAXsize_t is unsigned, so it wraps to a huge number
sizeof 'a'4character constants have type int in C (char in C++)
0x00000000 0 0xFFFFFFFF 4294967295 2147483647 0x7FFFFFFF 2147483648 0x80000000 1073741824 0x40000000 3221225472 0xC0000000 +1 0 −1 2147483647 −2147483648 1073741824 −1073741824 wraps UB outside: the bit pattern read as unsigned outside: read as unsigned · inside: read as int unsigned u; // 0 … UINT_MAX, counting clockwise int i; // same bits; top bit set means negative UINT_MAX + 1u == 0 // unsigned wraps: defined INT_MAX + 1 // signed overflow: undefined behavior -1 < 0u // false: -1 converts to 4294967295

1/5

An unsigned int is 32 bits: 4294967296 patterns, drawn here as a circle. Read as unsigned, they count clockwise from 0 to 4294967295 (UINT_MAX).

Same 32 bits, two readings: unsigned wraps at the top; signed overflow at the bottom is undefined.

C23 requires two's complement for signed integers (so INT_MIN is always -INT_MAX - 1), but signed overflow stays undefined: the optimizer assumes it never happens. To detect it, check before the operation or use C23's <stdckdint.h> (GCC 14+, Clang 18+):

#include <limits.h>
#include <stdckdint.h>   // C23 checked arithmetic
#include <stdio.h>
 
int main(void) {
    int a = 2'000'000'000, b = 2'000'000'000, sum;
    if (ckd_add(&sum, a, b))      // true if it overflowed
        puts("overflow");   // sum holds the wrapped value
 
    unsigned u = 0;
    u -= 1;                       // fine: wraps to UINT_MAX
    printf("%u %d\n", u, u == UINT_MAX);
 
    double avg = (double)a / 3;   // cast first, then divide
    printf("%.2f\n", avg);
}

-Wconversion -Wsign-conversion make the compiler point out every implicit narrowing or signed/unsigned mix; noisy on old code, worth it on new code.

Constants and literals

LiteralTypeNote
42, 42u, 42l, 42ul, 42llint, unsigned, long, unsigned long, long longan unsuffixed decimal gets the first signed type that fits
0x2A, 052, 0b101010inthex, octal (a leading zero!), binary (C23)
1'000'000intC23 digit separators
3.14, 3.14f, 1e-9, 0x1p-4double, float, double, doublethe last is a hex float, exactly 1/16
'a', '\n', '\0', '\x41'intsingle quotes are character codes
"hi"char[3]an array including the terminating '\0'; modifying it is UB
"ab" "cd"char[5]adjacent literals are joined at compile time
u8"é", u"é", U"é", L"é"UTF-8, UTF-16, UTF-32, wideu8 literals are char8_t (unsigned char) arrays in C23
nullptrnullptr_tC23 null pointer constant
true, falseboolC23 keywords (int 1 and 0 from <stdbool.h> before)

Four ways to name a constant:

#include <stdint.h>
#include <stdio.h>
 
#define MAX_USERS 100        // text substitution, no type
enum { BUF_SIZE = 256 };     // a true int constant, OK in
                             // array sizes and case labels
constexpr int kRetries = 3;  // C23: typed compile-time value
constexpr double kPi = 3.14159265358979;
const int limit = 10;        // a read-only variable, NOT a
                             // constant expression in C
 
char buf[BUF_SIZE];
int attempts[kRetries];
static_assert(sizeof buf == 256);   // C23: no message needed
 
enum color { RED, GREEN = 5, BLUE };       // 0, 5, 6
enum perm : uint8_t { P_READ = 1, P_WRITE = 2 }; // C23
 
int main(void) {
    enum perm p = P_READ | P_WRITE;
    printf("%d %.4f %zu %d\n", kRetries, kPi, sizeof p,
           BLUE);
}   // 3 3.1416 1 6
KeywordMeaning
constcannot be modified through this name; unlike C++, const int n = 4; is not a constant expression, so int a[n]; at block scope makes a VLA
constexprC23, objects only (not functions): the value is fixed at compile time and usable anywhere a constant is needed (GCC 13+, Clang 19+)
enumnamed int constants; an enum variable accepts any integer, there is no checking
enum E : TC23: chooses the underlying integer type (GCC 13+, Clang 20+)
#definepreprocessor text replacement; still the norm in older code and headers
static_assert(expr, "msg")compile-time check; C11 spelling _Static_assert

Operators and precedence

Most operators work as in TypeScript. The traps are in precedence and in what is left undefined.

ExpressionParses asFix
x & MASK == 0x & (MASK == 0)(x & MASK) == 0: bitwise ops bind looser than ==
x << 1 + 1x << 2(x << 1) + 1
*p++*(p++): read, then advance the pointer(*p)++ to increment the target
!x == y(!x) == y!(x == y) or x != y
a = b == ca = (b == c)fine if intended, otherwise parenthesize
if (x = 0)assignment, always falseif (x == 0); -Wall warns
i = i++ + 1undefined behaviormodify a variable once per expression
f(next(), next())argument order is unspecifiedcall into locals first
-7 >> 1implementation-defined (-4 on GCC/Clang)shift unsigned values
sizeof x + 1(sizeof x) + 1sizeof is an operator; parentheses needed only for types
x & MASK == 0 meant: (x & MASK) == 0 parsed: x & (MASK == 0) == & & == 0 x 2 MASK 4 x 2 MASK 4 0 → 1 → 0 → 0 → 0 with x = 2 and MASK = 4: the bit is clear, so the test should give 1 == and != bind tighter than &, ^ and |: parenthesize every bit test gcc -Wall and clang warn: -Wparentheses
== and != bind tighter than &, ^ and |: parenthesize every bit test.
Precedence (high → low)OperatorsAssociativity
postfixa() a[i] s.m p->m x++ x--left
unary! ~ -x +x ++x --x *p &x (type) sizeof alignofright
multiplicative* / %left
additive+ -left
shift<< >>left
relational< <= > >=left
equality== !=left
bitwise&, then ^, then |left
logical&&, then || (short-circuit)left
conditionalc ? a : bright
assignment= += -= *= /= %= <<= >>= &= ^= |=right
commaa, b (evaluates both, yields b)left

&& and || short-circuit and always yield int 0 or 1, never an operand (unlike Python's and/or). x++ yields the old value, ++x the new one. a % b takes the sign of a. There is no **: use pow() from <math.h>, which works on double.

Control flow

#include <stdio.h>
 
int main(void) {
    const char *text = "a quick test, ok?";
    int vowels = 0, spaces = 0, other = 0;
 
    for (const char *c = text; *c != '\0'; c++) {
        switch (*c) {            // integers and chars only
        case 'a': case 'e': case 'i': case 'o': case 'u':
            vowels++;
            break;               // without it: falls through
        case ' ':
            spaces++;
            [[fallthrough]];     // C23: intentional
        default:
            other++;
        }
    }
    printf("%d vowels, %d spaces, %d other\n",
           vowels, spaces, other);
 
    int n = 10;
    while (n > 1) n = (n % 2) ? 3 * n + 1 : n / 2;
    do { n--; } while (n > 0);   // body runs at least once
}
switch (*c) jumps to the matching label case 'a' … 'u': case ' ': default: *c is a vowel *c is a space anything else vowels++; break; spaces++; other++; end of switch falls through [[fallthrough]]; break jumps past the rest labels are only entry points: the code runs on until a break or the } "a quick test, ok?" gives 5 vowels, 3 spaces, 12 other: each space counts twice
case labels are only entry points: without break, execution runs into the next case.
ConstructNote
if (n), if (p), while (*s)any scalar works as a condition: zero or null is false
for (int i = 0; i < n; i++)i is scoped to the loop (C99); there is no for-each
switchinteger and character values only, no strings; case labels must be constant expressions
fallthroughmissing break runs the next case; GCC's -Wextra warns unless marked [[fallthrough]]; (Clang needs -Wimplicit-fallthrough)
break, continueinnermost loop only; no labeled break, so nested loops use a flag or goto
goto label;jumps within a function; idiomatic for cleanup on error

goto for cleanup: one exit path that releases everything acquired so far, in reverse order. Initialize every resource to a "nothing" value first so the cleanup code can tell what to release:

#include <stdio.h>
 
// Copy a file; returns 0 on success, -1 on any error.
int copy_file(const char *from, const char *to) {
    int rc = -1;
    FILE *in = nullptr, *out = nullptr;
    char buf[4096];
    size_t n;
 
    if (!(in = fopen(from, "rb"))) goto done;
    if (!(out = fopen(to, "wb"))) goto done;
    while ((n = fread(buf, 1, sizeof buf, in)) > 0)
        if (fwrite(buf, 1, n, out) != n) goto done;
    if (ferror(in)) goto done;
    rc = 0;
done:
    if (out && fclose(out) != 0) rc = -1;
    if (in) fclose(in);
    return rc;
}

The same pattern with heap memory is in Patterns.

Functions

#include <stdio.h>
 
// Prototype: lets main call it before the definition
static double average(const int *xs, size_t n);
 
static void swap(int *a, int *b) {  // pointers: modify
    int t = *a;                      // the caller's values
    *a = *b;
    *b = t;
}
 
static int next_id(void) {
    static int id = 0;     // static local: keeps its value
    return ++id;           // between calls
}
 
int main(void) {
    int scores[] = {90, 72, 85};
    printf("%.1f\n", average(scores, 3));   // 82.3
    int x = 1, y = 2;
    swap(&x, &y);
    printf("%d %d %d\n", x, y, next_id());  // 2 1 1
}
 
static double average(const int *xs, size_t n) {
    if (n == 0) return 0.0;
    long long sum = 0;
    for (size_t i = 0; i < n; i++) sum += xs[i];
    return (double)sum / (double)n;
}

Arguments are always passed by value: the function gets copies, including copies of whole structs. To let a function change the caller's variable, pass its address. Arrays are the exception that looks like pass-by-reference: an array argument becomes a pointer to its first element, so pass the length too.

FormMeaning
int f(void);takes no arguments; before C23, int f(); meant "unknown arguments", in C23 it means (void)
static int f(int);internal linkage: visible only in this .c file; use for helpers
int f(int); / extern int f(int);external linkage (the default): callable from other files
static inline int f(int x) { … }for small functions defined in headers; plain inline in C has subtle linking rules
[[nodiscard]] int f(void);C23: warn when the caller ignores the result
[[noreturn]] void die(const char *msg);C23: never returns (C11 _Noreturn is deprecated)
void f(int n, int a[static n])C99: promises at least n elements (documentation the compiler can check)
void cb(int, void *ctx)C23: unnamed parameters in a definition, for unused callback arguments
no overloading, no default argumentsone name, one function; _Generic dispatches on type

Variadic functions take ... and read the extra arguments with <stdarg.h>. Nothing checks their types unless you tell the compiler they follow printf rules:

#include <stdarg.h>
#include <stdio.h>
 
[[gnu::format(printf, 2, 3)]]   // GCC/Clang: check formats
void log_msg(const char *level, const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);     // C23 also allows va_start(ap)
    fprintf(stderr, "[%s] ", level);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fputc('\n', stderr);
}
 
#define type_name(x) _Generic((x), \
    int: "int", double: "double", char *: "char *", \
    default: "other")
 
int main(void) {
    log_msg("info", "%d users, %s", 3, type_name(2.5));
}

Headers and multiple files

Each .c file is compiled alone into a .o; a header (.h) shares declarations between them. A declaration says a name exists and what type it has, and may appear many times. A definition creates the function body or the variable's storage, and must appear exactly once in the whole program.

stack-demo
stack-demo/Makefileinclude/stack.h  # the interface: types and prototypessrc/main.c   # uses the stackstack.c  # implements it
include/stack.h
#ifndef STACK_H          // include guard: the second
#define STACK_H          // #include of this file is empty
 
#include <stddef.h>      // size_t
 
#define STACK_CAP 64
 
typedef struct {
    int items[STACK_CAP];
    size_t len;
} Stack;
 
void stack_init(Stack *s);
bool stack_push(Stack *s, int value);  // false when full
bool stack_pop(Stack *s, int *out);    // false when empty
 
extern long stack_pushes;  // declared here, defined in
                           // stack.c
#endif
src/stack.c
#include "stack.h"
 
long stack_pushes = 0;     // the one definition
 
static bool is_full(const Stack *s) {  // private helper
    return s->len == STACK_CAP;
}
 
void stack_init(Stack *s) { s->len = 0; }
 
bool stack_push(Stack *s, int value) {
    if (is_full(s)) return false;
    s->items[s->len++] = value;
    stack_pushes++;
    return true;
}
 
bool stack_pop(Stack *s, int *out) {
    if (s->len == 0) return false;
    *out = s->items[--s->len];
    return true;
}
src/main.c
#include <stdio.h>
#include "stack.h"
 
int main(void) {
    Stack s;
    stack_init(&s);
    for (int i = 1; i <= 3; i++) stack_push(&s, i * 10);
    int top;
    while (stack_pop(&s, &top)) printf("%d ", top);
    printf("\n%ld pushes\n", stack_pushes);
}   // prints "30 20 10", then "3 pushes"
cc -std=c23 -Wall -Wextra -Iinclude src/main.c src/stack.c \
   -o stack-demo
main.c stack.h stack.c uses the stack declarations only the definitions #include pastes it in compile compile main.o symbols stack.o symbols T main U printf libc U stack_init U stack_push U stack_pop U stack_pushes t is_full static T stack_init T stack_push T stack_pop B stack_pushes link each U → one T/B stack-demo nm letters: T function, B zeroed variable, U needed from elsewhere, t local no definition: undefined reference · two definitions: multiple definition
Each .c file is its own translation unit; only the linker matches every use to a definition.
TermMeaning
translation unitone .c file after the preprocessor has pasted in its headers
external linkagethe name is shared across translation units: functions and file-scope variables by default
internal linkagestatic at file scope: private to this translation unit, like an unexported module member
extern int n;a declaration of a variable defined elsewhere
int n; at file scopea tentative definition; two files each having one is a link error (GCC 10+ and Clang 11+ default to -fno-common)
include guard#ifndef X_H / #define X_H / #endif stops double inclusion within one translation unit
#pragma oncenon-standard alternative to guards; GCC, Clang and MSVC all support it

In a header: types, macros, prototypes, extern declarations and static inline functions. Never a function body or an initialized variable: every .c that includes it would define it again. bool needs no header in C23; C17 code includes <stdbool.h>.

Preprocessor

The preprocessor rewrites text before the compiler sees it: it knows nothing about types or scope.

#include <stdio.h>
 
#define SQUARE(x) ((x) * (x))     // parenthesize everything
#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
#define SWAP(a, b) do {           \
        typeof(a) tmp_ = (a);     \
        (a) = (b);                \
        (b) = tmp_;               \
    } while (0)                   // acts like one statement
#define STR(x) #x                 // stringize: "x"
#define CAT(a, b) a##b            // paste: ab
 
#ifdef DEBUG                      // cc -DDEBUG ...
#  define LOG(...) fprintf(stderr, __VA_ARGS__)
#else
#  define LOG(...) ((void)0)
#endif
 
int main(void) {
    int xs[] = {3, 1, 2};
    int a = 1, b = 2;
    SWAP(a, b);
    int CAT(total, _count) = SQUARE(a + 1);  // 9
    LOG("%s:%d\n", __FILE__, __LINE__);
    printf("%zu %s %d\n", ARRAY_LEN(xs), STR(a + b),
           total_count);
}
MacroCallExpands toProblem
#define SQ(x) x * xSQ(1 + 2)1 + 2 * 1 + 2 = 5parenthesize every parameter
#define TWICE(x) (x) + (x)3 * TWICE(2)3 * (2) + (2) = 8parenthesize the whole body
#define SQ(x) ((x) * (x))SQ(i++)((i++) * (i++))UB: the argument is evaluated twice; use a static inline function
#define CLEAN() free(a); free(b)if (x) CLEAN();only free(a) is conditionalwrap in do { … } while (0)
ARRAY_LEN(p)p is a pointersizeof p / sizeof *psilently wrong: arrays only

Prefer constexpr or enum for constants and static inline functions over function-like macros; keep macros for things only text substitution can do (stringizing, pasting, __FILE__, conditional code).

#if defined(_WIN32)
#  include <windows.h>
#elif defined(__linux__) || defined(__APPLE__)
#  include <unistd.h>
#endif
 
#if __STDC_VERSION__ >= 202311L    // compiling as C23?
#  define HAS_C23 1
#endif
 
#if __has_include(<threads.h>)     // C23 (earlier as an
#  include <threads.h>             // extension)
#endif
Directive or macroUse
#if, #ifdef, #ifndef, #elif, #else, #endifconditional compilation
#elifdef, #elifndef, #warning "msg"C23
#error "msg"stop the build with a message
__FILE__, __LINE__, __func__current file, line, function name (__func__ is a variable, not a macro)
__STDC_VERSION__201112L (C11), 201710L (C17), 202311L (C23)
_WIN32, __linux__, __APPLE__, __GNUC__, __clang__, _MSC_VERplatform and compiler detection
__VA_ARGS__, __VA_OPT__(,)variadic macro arguments; __VA_OPT__ is C23
#embed "file"C23: a file's bytes as a comma-separated list (GCC 15+, Clang 19+)

#embed replaces xxd -i and other code generators for bundling binary data:

static const unsigned char icon_png[] = {
#embed "icon.png"
};
// sizeof icon_png is the file size, known at compile time

Arrays and strings

An array is a fixed number of elements stored back to back. Its size is part of its type and must be known when it is created.

#include <stdio.h>
#include <string.h>
 
int main(void) {
    int primes[5] = {2, 3, 5, 7, 11};
    int zeros[100] = {0};      // missing elements are 0
    int counts[26] = {};       // C23 empty initializer
    double grid[3][4] = {};    // 3 rows of 4, contiguous
    int sparse[10] = {[2] = 1, [9] = 4};  // designated
 
    size_t n = sizeof primes / sizeof primes[0];   // 5
    for (size_t i = 0; i < n; i++) printf("%d ", primes[i]);
 
    int copy[5];
    memcpy(copy, primes, sizeof primes); // not copy = primes
    printf("\n%d %d %g %d %d\n", zeros[99], counts[0],
           grid[2][3], sparse[9], copy[4]);
}
  • Nothing checks indexes: primes[5] compiles and is undefined behavior.
  • Arrays cannot be assigned or compared with = and ==; use memcpy and memcmp.
  • Passed to a function, an array decays to a pointer to its first element and loses its size, so pass the length alongside. Why, and what sizeof does then, is in Pointer arithmetic and arrays.

A string is a char array whose text ends at the first '\0' byte (the null terminator). Every string function walks until it finds that byte, so a missing terminator makes them read past the end.

char name[8] = "Ada"; an 8-byte array on the stack [0] [1] [2] [3] [4] [5] [6] [7] A d a \0 \0 \0 \0 \0 name 0x7ffc…a40 strlen(name) == 3 sizeof name == 8 '\0' ends the string; the zero bytes after it are just spare room const char *p = "Ada"; a pointer to a read-only literal p 0x7ffc…a38 0x55…2004 A d a \0 0x55…2004 in .rodata sizeof p == 8 (the pointer) · strlen(p) == 3 · writing p[0] is undefined
strlen counts bytes before the terminator; sizeof measures the array, or just the pointer.
<string.h>DoesDanger
strlen(s)bytes before '\0'O(n) every call; s must be terminated
strcpy(dst, src)copy including '\0'knows nothing about dst's size: overflow
strncpy(dst, src, n)copy at most n bytesleaves dst unterminated when src has n or more bytes
strcat(dst, src)appendoverflow, and O(n) to find the end
strcmp(a, b)negative, 0 or positivea == b compares addresses, not text
strncmp(a, b, n)first n bytes only
strchr, strrchr, strstrfind a char (first, last) or substringreturn a pointer into the string, or null
strspn, strcspnlength of the prefix made of (or free of) given charss[strcspn(s, "\n")] = '\0' strips a newline
strtok(s, ",")split in placewrites '\0' into s, keeps hidden state: not reentrant
memcpy, memmove, memset, memcmpraw bytes, explicit lengthmemcpy with overlapping ranges is UB: use memmove
strdup(s), strndup(s, n)heap copy; caller must freestandard since C23 (POSIX before)
snprintf(buf, size, fmt, …)formatted write that never overflows and always terminates (size > 0)returns the length it wanted: compare to detect truncation
#include <stdio.h>
#include <string.h>
 
int main(void) {
    char name[16];
    int n = snprintf(name, sizeof name, "%s-%d", "user", 42);
    if (n < 0 || (size_t)n >= sizeof name)
        puts("truncated");
    printf("%s has %zu chars\n", name, strlen(name));
 
    if (strcmp(name, "user-42") == 0) puts("equal");
    const char *dash = strchr(name, '-');
    if (dash) printf("suffix: %s\n", dash + 1);
}

<ctype.h> classifies characters: isdigit, isalpha, isspace, toupper. Pass them an unsigned char value (isdigit((unsigned char)c)): a negative char is undefined behavior. Strings are bytes, not characters: strlen("é") is 2 in UTF-8.

Structs, unions and typedef

#include <stdio.h>
 
struct point { double x, y; };
 
typedef struct {               // anonymous struct + alias
    char name[32];
    int age;
    struct point home;
} Person;
 
static void move_home(Person *p, struct point to) {
    p->home = to;              // p->m is (*p).m
}
 
int main(void) {
    struct point origin = {0};             // all zero
    struct point q = {.y = 5.0};           // x is 0.0
    Person ada = {
        .name = "Ada",
        .age = 36,
        .home = {.x = 51.5, .y = -0.12},
    };
    Person copy = ada;          // copies every byte, the
    copy.age++;                 // name array included
    move_home(&ada, (struct point){.x = 3, .y = 4});
    printf("%s %d %d %.1f %.1f %.1f\n", ada.name, ada.age,
           copy.age, ada.home.x, q.y, origin.x);
}
FeatureNote
struct tag { … };defines a type named struct tag (struct tags have their own namespace)
typedef struct { … } Name;an alias so you can write Name instead of struct tag
.field = valuedesignated initializer (C99): any order, omitted fields are zero
(struct point){.x = 1}compound literal: an unnamed object, lives until the end of the enclosing block
s.m, p->mmember of a struct, member through a pointer
a = bcopies the whole struct; a == b does not compile: compare field by field
pass or return by valuecopies; pass const T * for large structs
unsigned ready : 1;bit-field: a member of N bits; layout is implementation-defined
memory layout and paddingsee Structs in memory

A union stores one of several members in the same bytes; its size is that of the largest member. Paired with a tag it becomes a tagged union, C's version of a TypeScript discriminated union:

#include <stdio.h>
 
typedef enum { SHAPE_CIRCLE, SHAPE_RECT } ShapeKind;
 
typedef struct {
    ShapeKind kind;            // says which member is live
    union {                    // anonymous union (C11)
        struct { double r; } circle;
        struct { double w, h; } rect;
    };
} Shape;
 
static double area(Shape s) {
    switch (s.kind) {
    case SHAPE_CIRCLE:
        return 3.14159265 * s.circle.r * s.circle.r;
    case SHAPE_RECT:
        return s.rect.w * s.rect.h;
    }
    return 0.0;
}
 
int main(void) {
    Shape shapes[] = {
        {.kind = SHAPE_CIRCLE, .circle = {.r = 1.0}},
        {.kind = SHAPE_RECT, .rect = {.w = 2, .h = 3}},
    };
    for (size_t i = 0; i < 2; i++)
        printf("%.2f\n", area(shapes[i]));
}

-Wswitch (in -Wall) warns when a switch on an enum misses a case and has no default. Names ending in _t are reserved by POSIX; pick another suffix for your own types.

Standard I/O

ConversionArgumentExample → output
%d, %iint (also char, short: they promote)printf("%d", -5) → -5
%uunsigned int
%ld, %lld, %lu, %llulong, long long, unsigned versions
%zu, %td, %jdsize_t, ptrdiff_t, intmax_tprintf("%zu", sizeof(int)) → 4
"%" PRId64, PRIu32, PRIx64int64_t, uint32_t, uint64_t in hexfrom <inttypes.h>
%x, %X, %o, %bunsigned in hex, HEX, octal, binary (%b is C23)printf("%#x", 255) → 0xff
%f, %.2f, %e, %gdouble (float promotes)printf("%.2f", 3.14159) → 3.14
%Lflong double
%ca character (as int)
%s, %.*sa null-terminated string; at most N bytes of oneprintf("%.*s", 3, "abcdef") → abc
%pa void * (cast other pointers)printf("%p", (void *)p) → 0x7ffc…
%%a literal %
%5d, %-5d, %05d, %+d, %*dwidth, left-align, zero-pad, always sign, width from an argumentprintf("[%5d]", 42) → [ 42]

A conversion that does not match its argument is undefined behavior. -Wall checks literal format strings; build format strings at runtime only if you must.

scanf is hard to use safely:

CaveatDetail
check the return valueit is the number of items assigned, or EOF; if (scanf("%d", &n) != 1)
bad input stays in the streama failed %d leaves the non-digits unread: loop and you spin forever
%s has no limitwrite a width: char w[32]; scanf("%31s", w);
numeric overflowan out-of-range number for %d is undefined behavior
%d leaves the newlinea later fgets reads an empty line
float vs double%f for float *, %lf for double * (unlike printf)

The robust pattern is to read a whole line with fgets and parse it with strtol or sscanf (recipe).

#include <stdio.h>
#include <string.h>
 
int main(void) {
    FILE *f = fopen("scores.txt", "w");
    if (f == nullptr) {
        perror("scores.txt");        // why it failed
        return 1;
    }
    fprintf(f, "ada 36\nlin 41\n");
    if (fclose(f) != 0) {            // flushes: can fail
        perror("fclose");
        return 1;
    }
 
    f = fopen("scores.txt", "r");
    if (!f) { perror("scores.txt"); return 1; }
    char name[32];
    int score;
    while (fscanf(f, "%31s %d", name, &score) == 2)
        printf("%-6s%4d\n", name, score);
    fclose(f);
}
ModeOpens forIf the file existsIf not
"r"readingread from the startfails
"w"writingtruncated to zerocreated
"a"appendingwrites go to the endcreated
"r+", "w+", "a+"reading and writingas aboveas above
"wx"writing, exclusive (C11)failscreated
add b ("rb")binary: no newline translation on Windows
  • fgets(buf, size, f) reads one line, keeps the '\n', returns null at end of file or on error.
  • fread/fwrite move raw bytes and return the number of items transferred.
  • while (!feof(f)) is a bug: feof only turns true after a read has already failed. Loop on the read call's return value, then check ferror(f) to tell an error from end of file.
  • stdout is line-buffered on a terminal and fully buffered into a pipe or file; stderr is unbuffered. fflush(stdout) forces output, e.g. before a prompt or a crash-prone call.

Error handling

C has no exceptions. Functions report failure through their return value, and the caller must check it.

ConventionExampleNotes
null pointerfopen, malloc, strchrtest with if (!p)
-1 or negativePOSIX read, close; printf0 or positive is success
EOFgetchar, fgetc, fscanf, fcloseEOF is a negative int; store getchar() in an int, not a char
countfread, fwrite, scanffewer than requested means end of file or error
bool + out-parameterbool parse_int(const char *s, int *out)keeps the error and the value separate
an error enumtypedef enum { OK, ERR_IO, ERR_PARSE } Err;callers can switch on it
errnoset by many library calls on failureonly meaningful right after a call that failed; success need not reset it
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef enum { ERR_OK, ERR_OPEN, ERR_READ } Err;
 
static const char *err_str(Err e) {
    switch (e) {
    case ERR_OK: return "ok";
    case ERR_OPEN: return "cannot open";
    case ERR_READ: return "read failed";
    }
    return "unknown";
}
 
[[nodiscard]] static Err first_line(const char *path,
                                    char *buf, int size) {
    FILE *f = fopen(path, "r");
    if (!f) {
        fprintf(stderr, "%s: %s\n", path, strerror(errno));
        return ERR_OPEN;
    }
    Err e = fgets(buf, size, f) ? ERR_OK : ERR_READ;
    fclose(f);
    return e;
}
 
int main(int argc, char *argv[]) {
    char line[256];
    Err e = first_line(argc > 1 ? argv[1] : "/etc/hostname",
                       line, sizeof line);
    if (e != ERR_OK) {
        fprintf(stderr, "error: %s\n", err_str(e));
        return EXIT_FAILURE;
    }
    fputs(line, stdout);
}
ToolUse
perror("ctx")prints ctx: No such file or directory from the current errno
strerror(errno)the same message as a string (may use a shared buffer, so not guaranteed thread-safe)
errno = 0; before a callneeded for functions like strtol whose return value cannot signal an error
ENOENT, EACCES, ENOMEM, EINVAL, ERANGE, EINTRcommon errno codes (<errno.h>)
assert(cond)<assert.h>: aborts with file and line when a programmer assumption fails; removed by -DNDEBUG
exit(EXIT_FAILURE)give up: fine in small programs, rude in libraries

C23 additions

FeatureExampleBefore C23
nullptr, nullptr_tint *p = nullptr;NULL, which may be plain 0
bool, true, false keywordsbool ok = true;<stdbool.h> macros
typeof, typeof_unqualtypeof(x) tmp = x;GNU __typeof__
auto type inferenceauto n = strlen(s); (size_t)none (auto was a useless storage class)
constexpr objectsconstexpr int N = 8;enum or #define
static_assert keyword, message optionalstatic_assert(sizeof(int) == 4);_Static_assert(e, "msg")
alignas, alignof, thread_local keywordsalignas(64) char buf[64];_Alignas etc. or <stdalign.h>
standard attributes[[nodiscard]], [[maybe_unused]], [[deprecated("use g")]], [[fallthrough]], [[noreturn]]__attribute__((…))
[[unsequenced]], [[reproducible]]mark pure functions for the optimizerGCC 15 supports them; Clang does not yet
empty initializerstruct s v = {}; (VLAs too)= {0}
#embedbinary file contents into an initializerxxd -i
#elifdef, #elifndef, #warning, __has_include, __VA_OPT__preprocessor additionscompiler extensions
binary literals, %b0b1010, printf("%b", 10u)GNU extension
digit separators1'000'000, 0xFF'FFnone
enum E : uint8_t { … }fixed underlying typenone
_BitInt(N)unsigned _BitInt(24) rgb;none
<stdckdint.h>ckd_add(&r, a, b) returns true on overflow__builtin_add_overflow
<stdbit.h>stdc_count_ones(x), stdc_bit_width(x)builtins
unreachable()in <stddef.h>: tells the optimizer a path cannot happen__builtin_unreachable()
strdup, strndup, memccpynow ISO CPOSIX only
memset_explicit(p, 0, n)a memset the optimizer may not remove, for wiping secretsexplicit_bzero (glibc, BSD); glibc has it from 2.43
int f() means int f(void)old K&R-style definitions are removedunknown arguments
unnamed parametersvoid cb(int, void *ctx)name them and cast to void
labels before declarations and at the end of a blockdone: }needed a ;
two's complementsigned integers are two's complementones' complement and sign-magnitude allowed in theory
realloc(p, 0)undefined behaviorimplementation-defined
__STDC_VERSION__202311L201710L

Undefined behavior

The standard sorts non-portable code into three kinds:

  • Implementation-defined: each compiler picks and documents a behavior: sizeof(long), char signedness, right-shifting a negative number.
  • Unspecified: one of several outcomes, not documented and not necessarily consistent: the order in which function arguments are evaluated.
  • Undefined behavior (UB): no requirements at all. The optimizer assumes UB never happens and transforms code on that basis, so the result can be a crash, a wrong answer, a deleted check or a program that works until the next compiler upgrade.
#include <limits.h>
 
// Looks like an overflow check, but x + 1 overflowing is
// UB, so the compiler may assume it cannot happen and
// compile this to `return false` (GCC and Clang at -O2 do)
bool next_overflows_bad(int x) { return x + 1 < x; }
 
// Correct: compare before doing the arithmetic
bool next_overflows(int x) { return x == INT_MAX; }
 
bool add_overflows(int a, int b) {
    return (b > 0 && a > INT_MAX - b) ||
           (b < 0 && a < INT_MIN - b);
}
Undefined behaviorExampleCaught by
signed integer overflowINT_MAX + 1, -INT_MIN, INT_MIN / -1UBSan, -ftrapv, ckd_add
integer division or remainder by zeron / 0, n % 0UBSan
bad shift1 << 32 (32-bit int), x << -1, -1 << 1UBSan
out-of-bounds accessa[5] for int a[5], s[strlen(s) + 1]ASan, -fsanitize=bounds
null pointer dereference*p with p == nullptrcrash (usually), UBSan, -fanalyzer
use after free, double freesee Common memory bugsASan, Valgrind
reading an uninitialized localint x; if (x) …-Wall (-Wuninitialized), Valgrind, Clang's MemorySanitizer
modifying a string literalchar *s = "hi"; s[0] = 'H';crashes on most systems (literals are read-only)
unsequenced modificationi = i++;, a[i] = i++;-Wall (-Wsequence-point, Clang -Wunsequenced)
printf format mismatchprintf("%d", 3.0)-Wall (-Wformat)
using the value of a function that fell off its endmissing return-Wall (-Wreturn-type)
float → int out of range(int)1e10UBSan (-fsanitize=float-cast-overflow)
negative char to <ctype.h>isdigit(c) with c < 0cast to unsigned char
incompatible pointer access (strict aliasing)reading a float through an int *memcpy; see aliasing
data racetwo threads, one writing, no lock or atomicThreadSanitizer (-fsanitize=thread)

Defense: -Wall -Wextra always, -fsanitize=address,undefined in debug builds and tests, -Werror in CI, and a second compiler now and then (GCC and Clang warn about different things).

Building with make

make rebuilds only what changed. A rule names a target, the prerequisites it depends on, and a recipe (shell commands, each line indented with a tab). make runs a recipe when the target is missing or older than any prerequisite.

Makefile
CFLAGS = -std=c23 -Wall -Wextra -pedantic -g
 
app: main.o util.o            # target: prerequisites
	$(CC) $(CFLAGS) main.o util.o -o app
 
main.o: main.c util.h
	$(CC) $(CFLAGS) -c main.c
 
util.o: util.c util.h
	$(CC) $(CFLAGS) -c util.c
SyntaxMeaning
make, make appbuild the first target, or a named one
$@, $<, $^the target, the first prerequisite, all prerequisites
%.o: %.ca pattern rule for every object file
CC, CFLAGS, LDFLAGS, LDLIBSconventional variables (CC defaults to cc)
.PHONY: cleantargets that are not files
make -j8run up to 8 recipes in parallel
make -nprint the commands without running them

A full Makefile with automatic header dependencies is in the recipes. Larger projects, and anything that must build on Windows too, usually use CMake to generate the build files:

CMakeLists.txt
cmake_minimum_required(VERSION 3.21)  # first with C 23
project(app LANGUAGES C)
 
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)           # -std=c23, not gnu23
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # for clangd
 
add_executable(app src/main.c src/stack.c)
target_include_directories(app PRIVATE include)
target_compile_options(app PRIVATE -Wall -Wextra -pedantic)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/app

Coming from Python or TypeScript

ConceptPythonTypeScriptC
runpython app.pynode app.js after tsccompile and link to a native binary, then run it
typesdynamicstatic, erased at runtimestatic, and they decide memory layout
integersunlimited sizenumber (a double), bigintfixed width; unsigned wraps, signed overflow is UB
x // y, x % yfloor divisionMath.floor(x / y)x / y truncates toward zero
x ** ybuilt inbuilt inpow(x, y) on double
stringsimmutable Unicode strimmutable UTF-16 stringmutable char arrays ending in '\0'; you manage the space
len(xs), xs.lengthbuilt inbuilt insizeof xs / sizeof xs[0] for arrays in scope; otherwise pass the length
listslistArrayfixed arrays; grow with realloc yourself
dicts, setsbuilt inMap, Setnone in the standard library: write one or use a library
memorygarbage collectedgarbage collectedmalloc/free by hand (memory sheet)
errorsexceptionsexceptionsreturn values and errno
nullNonenull, undefinednullptr / NULL; dereferencing one is UB
out-of-range indexIndexErrorundefinedundefined behavior
modulesimportimport / export#include a header, link the object files
visibility_name conventionexportstatic hides a name inside its file
genericsduck typingtype parametersvoid *, macros, _Generic
formattingf-stringstemplate literalsprintf, snprintf
and, or, notkeywords&&, ||, !&&, ||, !, giving int 0 or 1
packagespipnpmsystem packages, vcpkg, Conan, pkg-config

Recipes

Read a file line by line

For text files of any size. Lines longer than the buffer arrive in pieces; POSIX getline (with #define _POSIX_C_SOURCE 200809L before the includes) allocates as needed instead.

number.c
#include <stdio.h>
#include <string.h>
 
int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "usage: %s FILE\n", argv[0]);
        return 2;
    }
    FILE *f = fopen(argv[1], "r");
    if (!f) { perror(argv[1]); return 1; }
 
    char line[1024];
    long n = 0;
    while (fgets(line, sizeof line, f)) {
        line[strcspn(line, "\r\n")] = '\0';  // strip EOL
        printf("%4ld  %s\n", ++n, line);
    }
    int failed = ferror(f);
    fclose(f);
    if (failed) { perror("read"); return 1; }
}

Parse integers safely

Instead of atoi (no error reporting, UB on overflow) or scanf("%d").

#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
 
// True and *out set if s is exactly one base-10 int.
bool parse_int(const char *s, int *out) {
    char *end;
    errno = 0;
    long v = strtol(s, &end, 10);
    if (end == s) return false;            // no digits
    if (*end != '\0' && *end != '\n')
        return false;                      // trailing junk
    if (errno == ERANGE || v < INT_MIN || v > INT_MAX)
        return false;                      // out of range
    *out = (int)v;
    return true;
}
 
int main(void) {
    const char *tests[] = {"42", "-7", "12abc", "",
                           "99999999999"};
    for (size_t i = 0; i < 5; i++) {
        int v;
        if (parse_int(tests[i], &v)) printf("ok %d\n", v);
        else printf("bad '%s'\n", tests[i]);
    }
}

strtol skips leading whitespace and accepts a sign; base 0 also accepts 0x hex and 0 octal prefixes. Use strtoll, strtoul or strtod for other types.

Build a string piece by piece

When the output length is unknown: a growable buffer with printf-style appends.

strbuf.c
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
 
typedef struct {
    char *data;     // always null-terminated once non-empty
    size_t len;     // bytes used, excluding the '\0'
    size_t cap;     // bytes allocated
} StrBuf;
 
[[gnu::format(printf, 2, 3)]]
bool sb_printf(StrBuf *sb, const char *fmt, ...) {
    va_list ap, ap2;
    va_start(ap, fmt);
    va_copy(ap2, ap);
    int n = vsnprintf(nullptr, 0, fmt, ap);  // measure
    va_end(ap);
    if (n < 0) { va_end(ap2); return false; }
 
    size_t need = sb->len + (size_t)n + 1;
    if (need > sb->cap) {
        size_t cap = sb->cap ? sb->cap : 64;
        while (cap < need) cap *= 2;
        char *p = realloc(sb->data, cap);
        if (!p) { va_end(ap2); return false; }
        sb->data = p;
        sb->cap = cap;
    }
    vsnprintf(sb->data + sb->len, sb->cap - sb->len, fmt,
              ap2);
    va_end(ap2);
    sb->len += (size_t)n;
    return true;
}
 
void sb_free(StrBuf *sb) {
    free(sb->data);
    *sb = (StrBuf){};
}
 
int main(void) {
    StrBuf sb = {};
    for (int i = 1; i <= 5; i++)
        if (!sb_printf(&sb, "%s%d", i > 1 ? "," : "", i))
            return 1;
    printf("%s (%zu bytes)\n", sb.data, sb.len);
    sb_free(&sb);
}

Parse command-line flags

For small tools: a hand-written loop handles -v, -n NUM, -- and positional arguments in ISO C. POSIX getopt (in <unistd.h>) does the same with less code on Unix-like systems.

greet.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
static void usage(const char *prog) {
    fprintf(stderr, "usage: %s [-v] [-n COUNT] NAME...\n",
            prog);
}
 
int main(int argc, char *argv[]) {
    bool verbose = false;
    long count = 1;
    int i = 1;
    for (; i < argc && argv[i][0] == '-'; i++) {
        const char *a = argv[i];
        if (strcmp(a, "--") == 0) { i++; break; }
        if (strcmp(a, "-v") == 0) {
            verbose = true;
        } else if (strcmp(a, "-n") == 0 && i + 1 < argc) {
            char *end;
            count = strtol(argv[++i], &end, 10);
            if (*end != '\0' || count < 1 || count > 100) {
                usage(argv[0]);
                return 2;
            }
        } else if (strcmp(a, "-h") == 0) {
            usage(argv[0]);
            return 0;
        } else {
            fprintf(stderr, "unknown option %s\n", a);
            usage(argv[0]);
            return 2;
        }
    }
    if (i == argc) { usage(argv[0]); return 2; }
    for (; i < argc; i++)
        for (long k = 0; k < count; k++)
            printf(verbose ? "Hello there, %s!\n"
                           : "hi %s\n", argv[i]);
}

Start a Makefile project

A layout and Makefile that scale to dozens of files: objects go to build/, and -MMD -MP make every object depend on the headers it includes, so editing a header rebuilds the right files.

project
project/Makefileinclude/stack.hsrc/main.cstack.cbuild/  # generated: objects and .d files
Makefile
CFLAGS  := -std=c23 -Wall -Wextra -pedantic -Iinclude
CFLAGS  += -MMD -MP
LDLIBS  := -lm
 
ifeq ($(DEBUG),1)
  CFLAGS += -g -O0 -fsanitize=address,undefined
  LDFLAGS += -fsanitize=address,undefined
else
  CFLAGS += -O2 -DNDEBUG
endif
 
SRC := $(wildcard src/*.c)
OBJ := $(SRC:src/%.c=build/%.o)
 
app: $(OBJ)
	$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
 
build/%.o: src/%.c | build
	$(CC) $(CFLAGS) -c $< -o $@
 
build:
	mkdir -p $@
 
clean:
	rm -rf build app
 
.PHONY: clean
-include $(OBJ:.o=.d)
make -j"$(nproc)"     # release build
make clean
make DEBUG=1          # sanitizers and debug info

| build makes the directory an order-only prerequisite: it must exist, but its timestamp never forces a rebuild.

Compile with sanitizers

Every debug build and test run: AddressSanitizer catches memory errors and UBSan catches undefined arithmetic at the line where they happen, instead of a crash somewhere later.

gcc -std=c23 -g -O1 -fno-omit-frame-pointer \
    -fsanitize=address,undefined overflow.c -o overflow
./overflow
 
# Stop at the first UBSan report instead of continuing
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ./overflow
overflow.c
#include <limits.h>
#include <stdio.h>
 
int main(int argc, char *argv[]) {
    (void)argv;
    int big = INT_MAX - 1 + argc;   // INT_MAX at runtime
    int idx[4] = {0};
    printf("%d\n", big + 1);        // UB: signed overflow
    printf("%d\n", idx[argc + 3]);  // UB: index 4 of 4
}
overflow.c:8:5: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
overflow.c:9:23: runtime error: index 4 out of bounds for type 'int [4]'
=================================================================
==5875==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fdf53300030 ...
READ of size 4 at 0x7fdf53300030 thread T0
    #0 0x560d665a0532 in main overflow.c:9

ASan and Valgrind cannot be combined, and ThreadSanitizer (-fsanitize=thread) needs its own build. More tools and their output are in Debugging tools.

Map enum values to names with X-macros

When an enum and its string names must stay in sync: list the values once and let the preprocessor generate both.

#include <stdio.h>
 
#define COLORS(X) X(RED) X(GREEN) X(BLUE)
 
#define AS_ENUM(name) COLOR_##name,
#define AS_NAME(name) [COLOR_##name] = #name,
 
typedef enum { COLORS(AS_ENUM) COLOR_COUNT } Color;
 
static const char *const color_names[] = {COLORS(AS_NAME)};
 
int main(void) {
    for (int c = 0; c < COLOR_COUNT; c++)
        printf("%d = %s\n", c, color_names[c]);
}

Time a piece of code

For quick benchmarks: timespec_get (C11) gives wall-clock time with nanosecond resolution.

#include <stdio.h>
#include <time.h>
 
static double now_seconds(void) {
    struct timespec ts;
    timespec_get(&ts, TIME_UTC);
    return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
 
int main(void) {
    double t0 = now_seconds();
    volatile double x = 0;            // keep the loop alive
    for (int i = 0; i < 10'000'000; i++) x += i * 0.5;
    printf("%.3f ms\n", (now_seconds() - t0) * 1e3);
}

For CPU time instead of wall time use clock(); on POSIX, clock_gettime(CLOCK_MONOTONIC, …) is immune to clock changes.

References