C Programming Cheat Sheet
This C cheat sheet covers 30 sections: types and limits, operators and precedence, pointers, arrays and strings, malloc and free, structs and unions, the preprocessor, printf and scanf, files, the standard library, C23 features, threads and atomics, bit tricks, gcc flags, sanitizers, undefined behavior and the compiler errors you will actually meet. Search it, filter it by level or by C standard, copy any line with one click, and explain declarations, check printf formats or lint your code without leaving the page.
return 0;
Nothing matches that search. Try a shorter keyword such as malloc, printf or struct. Several words are combined, so every one of them has to match. Keys: / focuses the search, Esc clears it, t switches the theme.
Compile & Run
compilers, the build pipeline, a C toolbox and builders
Builders: Compiler Flags, Makefile, CMake, Integer Types, Debugging
Compile and Run
Hello, world, compiled and run
#include <stdio.h>
int main(void) {
puts("Hello, world");
return 0;
}
The commands
gcc -std=c23 -Wall -Wextra -g main.c -o hello ./hello clang -std=c23 -Wall -Wextra -g main.c -o hello gcc --version
Visual Studio command line
cl /std:clatest /W4 main.c main.exe
Try code without installing anything
# Compiler Explorer (godbolt.org): pick "C" and a compiler, see the assembly and run the program # a container with GCC: docker run --rm -v "$PWD":/src -w /src gcc:15 gcc -std=c23 -Wall main.c -o main
The Build Pipeline
Stop after each stage
gcc -E main.c -o main.i # preprocess only: #include and #define expanded gcc -S main.c -o main.s # compile to assembly gcc -c main.c -o main.o # assemble to an object file gcc main.o util.o -o app -lm # link objects and libraries
Multiple files and headers
// util.h
#ifndef UTIL_H
#define UTIL_H
int add(int a, int b);
#endif
// util.c
#include "util.h"
int add(int a, int b) { return a + b; }
// main.c
#include <stdio.h>
#include "util.h"
int main(void) { printf("%d\n", add(2, 3)); }
Libraries: order matters
gcc main.o -o app -L./lib -lfoo -lm gcc main.o -o app $(pkg-config --cflags --libs libcurl) ar rcs libutil.a util.o str.o # a static library gcc -shared -fPIC util.c -o libutil.so # a shared library
Program Structure
main, headers, comments, identifiers, exit codes and command line arguments
main and Arguments
argc and argv
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("%d arguments\n", argc);
for (int i = 0; i < argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}
Exit codes
#include <stdlib.h>
int main(void) {
if (!init()) return EXIT_FAILURE;
atexit(cleanup); // runs on return from main or exit()
exit(EXIT_SUCCESS);
}
Syntax Basics
Comments, statements and blocks
/* a block comment,
over several lines */
// a line comment (C99+)
int total = 0; // a declaration with an initializer
{ // a block opens a new scope
int total = 1; // shadows the outer total
}
Keywords of C23
| Group | Keywords |
|---|---|
| Types | void char short int long float double signed unsigned bool _BitInt _Complex |
| Type building | struct union enum typedef typeof typeof_unqual |
| Qualifiers | const volatile restrict _Atomic |
| Storage | auto extern static register thread_local constexpr |
| Control | if else switch case default for while do break continue goto return |
| Other | sizeof alignof alignas static_assert inline nullptr true false _Generic _Noreturn |
Identifiers you must not use
int _Count; // reserved: underscore followed by a capital letter int __total; // reserved: two underscores int str_len; // fine; but names starting with str, mem or is + lower case letter are reserved for <string.h> and <ctype.h>
Headers You Will Use
The standard headers
| Header | For |
|---|---|
<stdio.h> | printf, scanf, files |
<stdlib.h> | malloc, free, exit, strtol, qsort, rand |
<string.h> | strlen, strcpy, memcpy, memset, strcmp |
<stdint.h> <inttypes.h> | int32_t, uint64_t and their printf macros |
<stdbool.h> | bool, true, false (built in since C23) |
<stddef.h> | size_t, ptrdiff_t, NULL, offsetof |
<limits.h> <float.h> | INT_MAX, CHAR_BIT, DBL_EPSILON |
<ctype.h> | isalpha, isdigit, toupper |
<math.h> | sqrt, pow, sin, floor (link with -lm on Linux) |
<errno.h> | errno and error codes |
<assert.h> | assert, static_assert |
<time.h> | time, clock, strftime |
<threads.h> <stdatomic.h> | C11 threads and atomics |
<stdckdint.h> <stdbit.h> | C23 checked arithmetic and bit utilities |
Reading Input Arguments
Parse numeric arguments safely
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main(void) {
const char *argv1 = "250"; // stands in for argv[1]
char *end;
errno = 0;
long n = strtol(argv1, &end, 10);
if (errno || *end != '\0' || n < 1 || n > 1000) {
fprintf(stderr, "count must be 1 to 1000\n");
return EXIT_FAILURE;
}
printf("processing %ld items\n", n);
return EXIT_SUCCESS;
}
Types & Limits
integer and floating types, sizes, limits, literals, conversions and fixed width types
Sizes on Real Platforms
sizeof in bytes for the common data models
| Type | Minimum by the standard | LP64 (Linux, macOS) | LLP64 (Windows x64) | ILP32 (32-bit) |
|---|---|---|---|---|
char | 1 | 1 | 1 | 1 |
short | 2 | 2 | 2 | 2 |
int | 2 | 4 | 4 | 4 |
long | 4 | 8 | 4 | 4 |
long long | 8 | 8 | 8 | 8 |
void *, size_t | none | 8 | 8 | 4 |
float / double | 4 / 8 in practice | 4 / 8 | 4 / 8 | 4 / 8 |
long double | at least double | 16 (x86-64), 16 (ARM64 Linux), 8 (macOS ARM64) | 8 | 12 (x86) |
Print sizes and limits
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
int main(void) {
printf("int %zu, long %zu, pointer %zu\n", sizeof(int), sizeof(long), sizeof(void *));
printf("INT_MAX %d, LONG_MAX %ld\n", INT_MAX, LONG_MAX);
printf("UINT64_MAX %llu, CHAR_BIT %d\n", (unsigned long long)UINT64_MAX, CHAR_BIT);
}
Literals
Integer and floating literals
int d = 42, o = 052, h = 0x2A, b = 0b101010; // binary literals are C23 long big = 1'000'000L; // digit separators are C23 unsigned u = 42u; unsigned long long ull = 42ULL; float f = 1.5f; double x = 1.5e-3; long double ld = 1.5L; char c = 'A', nl = '\n';
Character and string literals
char a = 'A'; // an int value of type char char s[] = "hi"; // 3 bytes: 'h', 'i', '\0' wchar_t w[] = L"hi"; char8_t u8s[] = u8"héllo"; // UTF-8, char8_t is C23 char16_t u16 = u'é'; char32_t u32 = U'🙂';
Escape sequences
| Escape | Meaning |
|---|---|
\n \t \r | newline, tab, carriage return |
\\ \' \" | backslash, quote, double quote |
\0 | the null character, the end of every string |
\xhh | a byte in hex |
\ooo | a byte in octal |
€ \U0001F642 | a Unicode character |
Conversions
Integer promotion and the usual conversions
#include <stdio.h>
int main(void) {
unsigned int u = 1;
int i = -1;
printf("%d\n", i < u); // -1 is converted to unsigned
char c = 200; // implementation defined: char is signed on x86
printf("%d\n", c);
printf("%d\n", 7 / 2); // integer division truncates
printf("%.2f\n", 7 / 2.0);
}
Explicit casts
double avg = (double)sum / count; // cast before dividing int rounded = (int)(x + 0.5); // truncates toward zero unsigned char byte = (unsigned char)value; // keeps the low 8 bits
Arbitrary width integers
#include <stdio.h>
int main(void) {
unsigned _BitInt(128) big = (unsigned _BitInt(128))1 << 100;
unsigned _BitInt(12) small = 4095;
small += 1; // wraps to 0
printf("%d %d\n", (int)(big >> 100), (int)small);
}
Floating Point
Precision and comparison
#include <stdio.h>
#include <math.h>
#include <float.h>
int main(void) {
double a = 0.1 + 0.2;
printf("%.17f\n", a);
printf("%d\n", a == 0.3);
printf("%d\n", fabs(a - 0.3) < 1e-9);
printf("%d %d\n", FLT_DIG, DBL_DIG);
}
Operators & Precedence
arithmetic, comparison, logical, bitwise, assignment and the precedence table
The Operators
Arithmetic and comparison
a + b a - b a * b a / b a % b // % needs integers; sign follows the left operand a == b a != b a < b a <= b a > b a >= b !a a && b a || b // && and || stop early (short circuit) cond ? x : y
Increment, assignment, comma
i++; ++i; i--; --i; x += 5; x -= 1; x *= 2; x /= 3; x %= 4; x <<= 1; x >>= 1; x &= m; x |= m; x ^= m; for (int i = 0, j = n - 1; i < j; i++, j--) // the comma operator
Precedence, highest first
| Level | Operators | Associativity |
|---|---|---|
| 1 | () [] . -> postfix ++ -- | left to right |
| 2 | prefix ++ --, unary + - ! ~, * &, casts, sizeof, alignof | right to left |
| 3 | * / % | left to right |
| 4 | + - | left to right |
| 5 | << >> | left to right |
| 6 | < <= > >= | left to right |
| 7 | == != | left to right |
| 8 | & | left to right |
| 9 | ^ | left to right |
| 10 | vertical bar, bitwise OR | left to right |
| 11 | && | left to right |
| 12 | double vertical bar, logical OR | left to right |
| 13 | ?: | right to left |
| 14 | = += -= and the other assignments | right to left |
| 15 | , | left to right |
Traps
Bitwise operators bind weaker than comparisons
if (flags & MASK == 0) // means flags & (MASK == 0) if ((flags & MASK) == 0) // what you meant x = a << 2 + 1; // a << 3
Order of evaluation is not left to right
printf("%d %d\n", i++, i++); // undefined behavior: i modified twice
a[i] = i++; // undefined behavior
f(g(), h()); // g and h run in an unspecified order
Integer division and remainder of negatives
#include <stdio.h>
int main(void) {
printf("%d %d\n", -7 / 2, -7 % 2);
printf("%d %d\n", 7 / -2, 7 % -2);
}
Short Circuit and the Conditional
&& and || skip the right side
#include <stdio.h>
static int calls = 0;
static int check(int v) { calls++; return v; }
int main(void) {
int *p = NULL;
if (p && *p > 0) puts("positive"); // *p is never read when p is NULL
int r = check(0) && check(1);
int s = check(1) || check(1);
printf("%d %d calls=%d\n", r, s, calls);
int max = 3 > 7 ? 3 : 7;
printf("%d\n", max);
}
Control Flow
if, switch, loops, break, continue, goto and C23 fallthrough
Conditions
if, else if, else
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else {
grade = 'C';
}
switch
switch (cmd) {
case 'q':
quit();
break;
case 'h':
case '?': // several labels share one body
help();
break;
case 'v':
verbose = true;
[[fallthrough]]; // intended, silences -Wimplicit-fallthrough (C23)
default:
run(cmd);
}
Loops
for, while, do while
for (int i = 0; i < n; i++) { sum += a[i]; }
while (fgets(line, sizeof line, fp)) { parse(line); }
do {
c = getchar();
} while (c != '\n' && c != EOF);
for (;;) { if (done()) break; } // an infinite loop
break and continue
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue; // skip even numbers
if (i > 7) break; // stop the loop
printf("%d ", i);
}
putchar('\n');
}
Leave nested loops
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
if (grid[i][j] == target) goto found;
puts("not found");
return;
found:
printf("at %d,%d\n", i, j);
One cleanup path with goto
int load(const char *path) {
int rc = -1;
FILE *f = fopen(path, "rb");
if (!f) goto out;
char *buf = malloc(4096);
if (!buf) goto close_file;
if (fread(buf, 1, 4096, f) == 0) goto free_buf;
rc = 0;
free_buf:
free(buf);
close_file:
fclose(f);
Arrays
declaring, initializing, sizing, passing and multi dimensional arrays
Declare and Initialize
Initializers
int a[5]; // uninitialized inside a function
int b[5] = {1, 2, 3}; // the rest are 0
int c[] = {10, 20, 30}; // size 3, counted by the compiler
int d[5] = {}; // all zero; the empty initializer is C23, {0} works everywhere
int e[10] = {[2] = 7, [9] = 1}; // designated initializers (C99)
char s[] = "abc"; // 4 bytes including the '\0'
Number of elements
#include <stdio.h>
int main(void) {
int scores[] = {90, 72, 88, 65};
size_t n = sizeof scores / sizeof scores[0];
printf("%zu elements\n", n);
for (size_t i = 0; i < n; i++) printf("%d ", scores[i]);
putchar('\n');
}
Arrays and Functions
An array parameter is a pointer
#include <stdio.h>
void show(int arr[], size_t n) { // same as int *arr
printf("inside: %zu\n", sizeof arr); // the size of a pointer
for (size_t i = 0; i < n; i++) printf("%d ", arr[i]);
putchar('\n');
}
int main(void) {
int v[] = {1, 2, 3, 4, 5};
printf("outside: %zu\n", sizeof v);
show(v, sizeof v / sizeof v[0]);
}
Document the minimum size in the parameter
void fill(size_t n, int buf[static n]); // buf has at least n elements, never null void sum3(const double v[static 3]);
Multi-dimensional Arrays
2D arrays are arrays of arrays
#include <stdio.h>
int main(void) {
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
for (int r = 0; r < 2; r++) {
for (int c = 0; c < 3; c++) printf("%d ", grid[r][c]);
putchar('\n');
}
printf("%zu bytes, row of %zu\n", sizeof grid, sizeof grid[0]);
}
Pass a 2D array with its sizes
void print_matrix(size_t rows, size_t cols, int m[rows][cols]) {
for (size_t r = 0; r < rows; r++)
for (size_t c = 0; c < cols; c++)
printf("%d%c", m[r][c], c + 1 == cols ? '\n' : ' ');
}
Variable length arrays
void work(size_t n) {
double tmp[n]; // on the stack, size decided at run time
for (size_t i = 0; i < n; i++) tmp[i] = 0.0;
}
Common Array Tasks
Reverse, find the maximum, copy
#include <stdio.h>
#include <string.h>
int main(void) {
int a[] = {3, 9, 2, 7, 5};
size_t n = sizeof a / sizeof a[0];
for (size_t i = 0, j = n - 1; i < j; i++, j--) { int t = a[i]; a[i] = a[j]; a[j] = t; }
int max = a[0];
for (size_t i = 1; i < n; i++) if (a[i] > max) max = a[i];
int copy[5];
memcpy(copy, a, sizeof a);
for (size_t i = 0; i < n; i++) printf("%d ", copy[i]);
printf("max=%d\n", max);
}
Strings
char arrays, string.h, copying safely, comparing, searching and splitting
Basics
A string is a char array ending in '\0'
#include <stdio.h>
#include <string.h>
int main(void) {
char word[] = "hello";
printf("%zu %zu\n", strlen(word), sizeof word);
word[0] = 'H';
printf("%s\n", word);
const char *lit = "fixed"; // points to read only memory
printf("%c\n", lit[1]);
}
Copy and join safely with snprintf
char path[64];
snprintf(path, sizeof path, "%s/%s.txt", dir, name); // always terminated, never overflows
int need = snprintf(NULL, 0, "%s/%s.txt", dir, name); // how many bytes it would take
if (need >= (int)sizeof path) { /* the result was truncated */ }
Compare
if (strcmp(a, b) == 0) { /* equal */ }
if (strncmp(line, "GET ", 4) == 0) { /* starts with GET */ }
if (strcasecmp(a, b) == 0) { /* equal ignoring case, POSIX */ }
Searching and Splitting
Search
#include <stdio.h>
#include <string.h>
int main(void) {
const char *s = "key=value; other=1";
const char *eq = strchr(s, '=');
const char *semi = strstr(s, "; ");
printf("%td %td\n", eq - s, semi - s);
printf("%zu\n", strspn("12345abc", "0123456789"));
printf("%.*s\n", (int)(eq - s), s);
}
Split into tokens
#include <stdio.h>
#include <string.h>
int main(void) {
char csv[] = "red,green,,blue";
for (char *tok = strtok(csv, ","); tok; tok = strtok(NULL, ","))
printf("[%s]\n", tok);
}
Duplicate a string
char *copy = strdup(src); // malloc + copy; standard since C23, POSIX before char *first5 = strndup(src, 5); // at most 5 characters, always terminated free(copy); free(first5);
Character classes
#include <ctype.h>
if (isdigit((unsigned char)c)) { }
if (isspace((unsigned char)c)) { }
c = (char)toupper((unsigned char)c);
Memory Functions
memcpy, memmove, memset, memcmp
memcpy(dst, src, n); // the ranges must not overlap
memmove(dst, dst + 1, n - 1); // overlapping ranges are fine
memset(buf, 0, sizeof buf); // fill bytes
if (memcmp(a, b, n) == 0) { } // compare bytes
Build a string of unknown length
char *s = NULL; size_t len = 0; FILE *m = open_memstream(&s, &len); // POSIX fprintf(m, "%d items, total %.2f\n", n, total); fclose(m); // s now holds the text free(s);
String Tasks
Trim, lower case, and count words
#include <stdio.h>
#include <ctype.h>
#include <string.h>
static char *trim(char *s) {
while (isspace((unsigned char)*s)) s++;
char *end = s + strlen(s);
while (end > s && isspace((unsigned char)end[-1])) end--;
*end = '\0';
return s;
}
int main(void) {
char text[] = " Hello Big WORLD ";
char *t = trim(text);
for (char *p = t; *p; p++) *p = (char)tolower((unsigned char)*p);
int words = 0, in = 0;
for (char *p = t; *p; p++) { if (isspace((unsigned char)*p)) in = 0; else if (!in) { in = 1; words++; } }
printf("[%s] %d words\n", t, words);
}
Pointers
addresses, dereferencing, pointer arithmetic, void pointers, pointers to pointers and null
Basics
Address of, dereference
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x; // p holds the address of x
*p = 20; // write through the pointer
printf("%d %d\n", x, *p);
printf("%d\n", p == &x);
}
Null pointers
int *p = NULL; // or nullptr in C23
if (p != NULL) { use(*p); }
if (!p) { /* p is null */ }
free(p); // free(NULL) is a no-op
Pass by pointer to change the caller's variable
#include <stdio.h>
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
int main(void) {
int x = 1, y = 2;
swap(&x, &y);
printf("%d %d\n", x, y);
}
Pointer Arithmetic
Moving through an array
#include <stdio.h>
#include <stddef.h>
int main(void) {
int a[] = {10, 20, 30, 40};
int *p = a; // same as &a[0]
printf("%d %d %d\n", *p, *(p + 2), p[3]);
int *end = a + 4; // one past the end: valid to form, not to read
ptrdiff_t n = end - p;
printf("%td\n", n);
for (int *q = a; q < end; q++) printf("%d ", *q);
putchar('\n');
}
void pointers
void *any = &x; // any object pointer converts to void * and back int *ip = any; // no cast needed in C size_t n = 10; void *mem = malloc(n); unsigned char *bytes = mem; // look at the raw bytes
Pointers to pointers
#include <stdio.h>
#include <stdlib.h>
void alloc_int(int **out, int v) {
*out = malloc(sizeof **out);
if (*out) **out = v;
}
int main(void) {
int *p = NULL;
alloc_int(&p, 42);
printf("%d\n", *p);
free(p);
}
const and Pointers
Read declarations right to left
const int *p; // pointer to const int: *p = 1 is an error, p = &y is fine int *const q = &x; // const pointer to int: *q = 1 is fine, q = &y is an error const int *const r; // both const char **argvp; // pointer to pointer to const char
Dangling pointers
int *bad(void) {
int local = 5;
return &local; // local dies when the function returns
}
char *s = malloc(8);
free(s);
s[0] = 'x'; // use after free
Pointers and Arrays of Strings
An array of pointers to strings
#include <stdio.h>
int main(void) {
const char *days[] = {"Mon", "Tue", "Wed"};
const char **p = days;
printf("%s %c %zu\n", p[1], *days[2], sizeof days / sizeof days[0]);
for (const char **d = days; d < days + 3; d++) printf("%s ", *d);
putchar('\n');
}
Dynamic Memory
malloc, calloc, realloc, free, ownership and leak free patterns
Allocate and Free
malloc and free
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *a = malloc(n * sizeof *a);
if (!a) { perror("malloc"); return 1; }
for (size_t i = 0; i < n; i++) a[i] = (int)(i * i);
for (size_t i = 0; i < n; i++) printf("%d ", a[i]);
putchar('\n');
free(a);
a = NULL;
}
calloc zeroes and checks the multiplication
int *counts = calloc(n, sizeof *counts); // all zero, and n * size cannot silently overflow struct node *nodes = calloc(count, sizeof *nodes);
realloc: grow without leaking
int *tmp = realloc(a, new_n * sizeof *a);
if (!tmp) { free(a); return -1; } // a is still valid when realloc fails
a = tmp;
Patterns
A growable array
#include <stdio.h>
#include <stdlib.h>
typedef struct { int *data; size_t len, cap; } IntVec;
int push(IntVec *v, int x) {
if (v->len == v->cap) {
size_t cap = v->cap ? v->cap * 2 : 8;
int *d = realloc(v->data, cap * sizeof *d);
if (!d) return -1;
v->data = d; v->cap = cap;
}
v->data[v->len++] = x;
return 0;
}
int main(void) {
IntVec v = {0};
for (int i = 0; i < 20; i++) push(&v, i);
printf("%zu items, capacity %zu\n", v.len, v.cap);
free(v.data);
}
Who frees what
char *make_greeting(const char *name); // returns memory the caller must free void set_name(struct user *u, const char *name); // copies name; u owns the copy const char *user_name(const struct user *u); // borrowed: do not free
Aligned and sized allocation
void *buf = aligned_alloc(64, 4096); // C11: alignment 64, size a multiple of it free(buf); void *p = malloc(100); free_sized(p, 100); // C23: tell the allocator the size
Stack or Heap
Where each object lives
| Declared as | Lives in | Lifetime |
|---|---|---|
| local variable, VLA | stack | until the block ends |
static local, global | static storage | the whole program |
malloc, calloc, realloc | heap | until free |
| string literal | read only static storage | the whole program |
| compound literal in a function | stack | until the block ends |
Debugging Allocations
A leak, found by AddressSanitizer
#include <stdlib.h>
#include <string.h>
char *copy_name(const char *s) { char *c = malloc(strlen(s) + 1); strcpy(c, s); return c; }
int main(void) {
char *a = copy_name("Ana");
a = copy_name("Radu"); // the first copy is lost
free(a);
}
Structs, Unions, Enums
declaring, initializing, typedef, padding, flexible arrays, unions and enums
Structs
Declare, initialize, use
#include <stdio.h>
struct point { double x, y; };
typedef struct {
char name[32];
int age;
struct point home;
} Person;
int main(void) {
Person p = {.name = "Ana", .age = 31, .home = {.x = 1.5, .y = -2.0}};
Person copy = p; // struct assignment copies every member
copy.age++;
printf("%s %d %d %.1f\n", p.name, p.age, copy.age, p.home.x);
Person *pp = &p;
printf("%s\n", pp->name); // -> for access through a pointer
}
Compound literals
struct point p = (struct point){.x = 3, .y = 4};
draw_line((struct point){0, 0}, (struct point){10, 10});
int *arr = (int[]){1, 2, 3};
Padding and offsetof
#include <stdio.h>
#include <stddef.h>
struct msg { char tag; double value; short id; };
struct packed_order { double value; short id; char tag; };
int main(void) {
printf("%zu %zu\n", sizeof(struct msg), sizeof(struct packed_order));
printf("%zu %zu\n", offsetof(struct msg, value), offsetof(struct msg, id));
}
Flexible array member
struct buffer {
size_t len;
unsigned char data[]; // must be last
};
struct buffer *b = malloc(sizeof *b + n);
b->len = n;
Bit-fields
struct flags {
unsigned int visible : 1;
unsigned int dirty : 1;
unsigned int level : 4;
};
Unions and Enums
A tagged union
#include <stdio.h>
enum kind { NUM, TEXT };
struct value {
enum kind kind;
union { double num; const char *text; } as;
};
void show(struct value v) {
switch (v.kind) {
case NUM: printf("%g\n", v.as.num); break;
case TEXT: printf("%s\n", v.as.text); break;
}
}
int main(void) {
show((struct value){.kind = NUM, .as.num = 2.5});
show((struct value){.kind = TEXT, .as.text = "hi"});
}
Enums
enum color { RED, GREEN = 5, BLUE }; // 0, 5, 6
enum status : unsigned char { OK, FAIL }; // C23: fixed underlying type
typedef enum { LOW = 1, HIGH = 2 } Level;
Type punning through a union
#include <stdio.h>
#include <stdint.h>
int main(void) {
union { float f; uint32_t u; } pun = {.f = 1.0f};
printf("0x%08x\n", (unsigned)pun.u);
}
Linked Structures
A singly linked list
#include <stdio.h>
#include <stdlib.h>
struct node { int value; struct node *next; };
static struct node *push(struct node *head, int v) {
struct node *n = malloc(sizeof *n);
if (!n) return head;
n->value = v; n->next = head;
return n;
}
int main(void) {
struct node *list = NULL;
for (int i = 1; i <= 4; i++) list = push(list, i * 10);
for (struct node *n = list; n; n = n->next) printf("%d -> ", n->value);
puts("NULL");
while (list) { struct node *next = list->next; free(list); list = next; }
}
Functions
declarations, prototypes, parameters, return values, recursion, inline and variadic functions
Declare and Define
Prototype first, definition anywhere
#include <stdio.h>
double average(const int *values, size_t n); // declaration (prototype)
int main(void) {
int v[] = {4, 8, 15, 16, 23, 42};
printf("%.2f\n", average(v, 6));
}
double average(const int *values, size_t n) { // definition
if (n == 0) return 0.0;
long sum = 0;
for (size_t i = 0; i < n; i++) sum += values[i];
return (double)sum / n;
}
No parameters: (void), or () in C23
int get_count(void); // takes no arguments, in every standard int get_count(); // C23: the same; before C23: unspecified parameters
Arguments are copied
#include <stdio.h>
void try_change(int n, int *p) { n = 99; *p = 99; }
int main(void) {
int a = 1, b = 1;
try_change(a, &b);
printf("%d %d\n", a, b);
}
More Function Features
Return several values through a struct
typedef struct { int quot, rem; bool ok; } DivResult;
DivResult divide(int a, int b) {
if (b == 0) return (DivResult){.ok = false};
return (DivResult){a / b, a % b, true};
}
Recursion
#include <stdio.h>
unsigned long long factorial(unsigned n) { return n <= 1 ? 1 : n * factorial(n - 1); }
int main(void) {
for (unsigned n = 0; n <= 20; n += 5) printf("%u! = %llu\n", n, factorial(n));
}
static and inline functions
static int helper(int x) { return x * 2; } // visible only in this file
static inline int square(int x) { return x * x; } // a hint to inline; put it in headers
Variadic functions
#include <stdarg.h>
#include <stdio.h>
int sum(int count, ...) {
va_list ap;
va_start(ap, count);
int total = 0;
for (int i = 0; i < count; i++) total += va_arg(ap, int);
va_end(ap);
return total;
}
int main(void) { printf("%d\n", sum(4, 1, 2, 3, 4)); }
Functions that never return
[[noreturn]] void die(const char *msg) {
fprintf(stderr, "fatal: %s\n", msg);
exit(EXIT_FAILURE);
}
Error Handling Conventions
Return a status, pass results through pointers
#include <stdio.h>
#include <errno.h>
#include <string.h>
typedef enum { OK = 0, ERR_ARG = -1, ERR_IO = -2 } status;
static status read_count(const char *path, int *out) {
if (!path || !out) return ERR_ARG;
FILE *f = fopen(path, "r");
if (!f) return ERR_IO;
int ok = fscanf(f, "%d", out) == 1;
fclose(f);
return ok ? OK : ERR_IO;
}
int main(void) {
int n;
status st = read_count("/nonexistent/count.txt", &n);
printf("status %d: %s\n", st, st == ERR_IO ? strerror(errno) : "ok");
}
Function Pointers
declaring, calling, arrays of handlers, callbacks, qsort and typedefs
Declare and Call
A pointer to a function
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int main(void) {
int (*op)(int, int) = add; // no & needed
printf("%d\n", op(3, 4)); // no * needed to call
op = mul;
printf("%d\n", op(3, 4));
}
A typedef makes it readable
typedef int (*binary_op)(int, int);
typedef void handler_fn(int signal); // a function type; use handler_fn *
binary_op table[] = {add, mul};
void on_signal(handler_fn *h);
Callbacks
qsort with a comparator
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int by_value(const void *a, const void *b) {
int x = *(const int *)a, y = *(const int *)b;
return (x > y) - (x < y); // avoids overflow of x - y
}
static int by_name(const void *a, const void *b) {
return strcmp(*(const char *const *)a, *(const char *const *)b);
}
int main(void) {
int v[] = {42, -7, 15, 3};
qsort(v, 4, sizeof v[0], by_value);
for (int i = 0; i < 4; i++) printf("%d ", v[i]);
const char *names[] = {"pear", "apple", "fig"};
qsort(names, 3, sizeof names[0], by_name);
printf("\n%s %s %s\n", names[0], names[1], names[2]);
}
A callback with context
typedef void (*visit_fn)(int value, void *ctx);
void for_each(const int *a, size_t n, visit_fn f, void *ctx) {
for (size_t i = 0; i < n; i++) f(a[i], ctx);
}
static void add_to(int v, void *ctx) { *(long *)ctx += v; }
long total = 0;
for_each(values, n, add_to, &total);
A dispatch table
typedef int (*cmd_fn)(int argc, char **argv);
struct command { const char *name; cmd_fn run; };
static const struct command commands[] = {
{"list", cmd_list},
{"add", cmd_add},
{"rm", cmd_remove},
};
for (size_t i = 0; i < sizeof commands / sizeof commands[0]; i++)
if (strcmp(argv[1], commands[i].name) == 0) return commands[i].run(argc - 1, argv + 1);
More Callback Patterns
A state machine with a table of handlers
#include <stdio.h>
typedef enum { IDLE, RUNNING, DONE, STATE_COUNT } state;
typedef state (*step_fn)(int input);
static state on_idle(int in) { return in > 0 ? RUNNING : IDLE; }
static state on_running(int in) { return in == 0 ? DONE : RUNNING; }
static state on_done(int in) { (void)in; return DONE; }
static const step_fn steps[STATE_COUNT] = {on_idle, on_running, on_done};
static const char *names[STATE_COUNT] = {"IDLE", "RUNNING", "DONE"};
int main(void) {
state s = IDLE;
int inputs[] = {0, 5, 3, 0, 9};
for (int i = 0; i < 5; i++) { s = steps[s](inputs[i]); printf("%s ", names[s]); }
putchar('\n');
}
Preprocessor
include, define, macros, conditional compilation, stringizing, token pasting and #embed
Includes and Macros
Includes
#include <stdio.h> // search the system directories #include "config.h" // search next to this file first, then the system directories #if __has_include(<threads.h>) #include <threads.h> #endif
Object and function-like macros
#define BUFFER_SIZE 4096 #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define SQUARE(x) ((x) * (x)) #define COUNT(arr) (sizeof (arr) / sizeof (arr)[0])
Multi-statement macros
#define LOG_ERR(msg) do { fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, (msg)); } while (0)
if (failed) LOG_ERR("open failed"); // works with a following else
else proceed();
Conditional Compilation
#if, #ifdef, #elifdef
#ifdef DEBUG
fprintf(stderr, "x = %d\n", x);
#endif
#if defined(_WIN32)
# include <windows.h>
#elif defined(__linux__)
# include <unistd.h>
#elifdef __APPLE__
# include <TargetConditionals.h>
#else
# error "unsupported platform"
#endif
Predefined macros
#include <stdio.h>
int main(void) {
printf("%s:%d in %s\n", __FILE__, __LINE__, __func__);
printf("C version %ld\n", __STDC_VERSION__);
#ifdef __GNUC__
printf("GCC compatible compiler %d.%d\n", __GNUC__, __GNUC_MINOR__);
#endif
}
Advanced Macros
Stringizing and token pasting
#include <stdio.h>
#define STR(x) #x
#define XSTR(x) STR(x)
#define VERSION 3
#define FIELD(name) int field_##name
struct cfg { FIELD(width); FIELD(height); };
int main(void) {
printf("%s %s\n", STR(VERSION), XSTR(VERSION));
struct cfg c = {.field_width = 800};
printf("%d\n", c.field_width);
}
Variadic macros
#define LOG(fmt, ...) fprintf(stderr, "[%s] " fmt "\n", __func__ __VA_OPT__(,) __VA_ARGS__)
LOG("starting");
LOG("loaded %d items from %s", n, path);
Embed a binary file
static const unsigned char logo_png[] = {
#embed "logo.png"
};
static const size_t logo_png_len = sizeof logo_png;
See what the preprocessor produced
gcc -E main.c | less gcc -dM -E - < /dev/null | sort # every predefined macro of the compiler
Storage & Linkage
scope, lifetime, static, extern, global variables and thread_local
Scope and Lifetime
A static local keeps its value
#include <stdio.h>
int next_id(void) {
static int id = 0; // initialized once, lives for the whole program
return ++id;
}
int main(void) {
int a = next_id();
int b = next_id();
int c = next_id();
printf("%d %d %d\n", a, b, c);
}
static and extern at file level
// config.c int verbose = 0; // definition, external linkage static int cache_size = 64; // internal linkage: private to config.c // config.h extern int verbose; // declaration only: defined in config.c // main.c #include "config.h" verbose = 1;
Storage durations
| Declared | Storage | Initialized |
|---|---|---|
| inside a block | automatic, until the block ends | not initialized unless you do it |
static inside a block | static, whole program | once, to zero if no initializer |
| at file scope | static, whole program | once, to zero if no initializer |
thread_local | one copy per thread | when the thread starts |
malloc | allocated, until free | not initialized (calloc zeroes) |
thread_local
thread_local int errors_this_thread = 0; // C23 keyword; _Thread_local in C11
Shadowing and Initialization
Static and automatic initialization
#include <stdio.h>
int global_count; // zero initialized
int main(void) {
static double table[3]; // zero initialized
int local = 0; // must be initialized explicitly
printf("%d %.1f %d\n", global_count, table[2], local);
{
int local = 5; // shadows the outer local: -Wshadow warns
printf("%d\n", local);
}
printf("%d\n", local);
}
Header only constants and inline functions
// limits.h of your project
static const int MAX_USERS = 100; // one private copy per .c file
static inline int clamp(int v, int lo, int hi) { return v < lo ? lo : v > hi ? hi : v; }
extern const char *const APP_NAME; // one shared definition in a .c file
const, volatile, restrict
qualifiers, const correctness, volatile for hardware and signals, restrict and constexpr
const
const correctness in function parameters
size_t count_spaces(const char *s); // promises not to modify *s void fill(int *out, size_t n, int value); // writes through out void copy(char *restrict dst, const char *restrict src, size_t n);
constexpr objects
constexpr int COLUMNS = 80; constexpr double PI = 3.141592653589793; char line[COLUMNS]; // a real constant: a normal array, not a VLA
volatile and restrict
volatile: every access really happens
volatile uint32_t *const status = (uint32_t *)0x40021000; // a hardware register
while ((*status & READY) == 0) { } // re-read on every loop
volatile sig_atomic_t stop = 0; // set from a signal handler
restrict: no overlap, faster code
void add_arrays(size_t n, float *restrict out, const float *restrict a, const float *restrict b) {
for (size_t i = 0; i < n; i++) out[i] = a[i] + b[i];
}
const in Practice
What const does and does not prevent
#include <stdio.h>
static size_t count_char(const char *s, char c) {
size_t n = 0;
for (; *s; s++) if (*s == c) n++; // moving s is fine; changing *s is not
return n;
}
int main(void) {
const int limit = 3;
int *sneaky = (int *)&limit; // casting const away compiles...
printf("%zu\n", count_char("banana", 'a'));
(void)sneaky; // ...but writing through it is undefined behavior
}
volatile is not atomic
volatile int ready = 0; // wrong for threads: no ordering, no atomicity atomic_int ready2 = 0; // right: use <stdatomic.h> between threads
printf & scanf
format specifiers, width and precision, reading input safely
printf
The common conversions
| Specifier | Argument | Example output |
|---|---|---|
%d %i | int | -42 |
%u | unsigned int | 42 |
%ld %lld | long, long long | 9000000000 |
%zu | size_t | 16 |
%td | ptrdiff_t | -3 |
%x %X %o | unsigned, in hex or octal | ff FF 377 |
%b | unsigned, in binary (C23) | 1010 |
%f %e %g | double (and float, promoted) | 3.140000 3.140000e+00 3.14 |
%c | int holding a character | A |
%s | char * to a string | hello |
%p | void * | 0x7ffd5b2c |
%% | none | % |
Width, precision and flags
#include <stdio.h>
int main(void) {
printf("[%5d] [%-5d] [%05d]\n", 42, 42, 42);
printf("[%.2f] [%8.3f] [%+.1e]\n", 3.14159, 3.14159, 31415.9);
printf("[%x] [%#x] [%08X]\n", 255, 255, 255);
printf("[%.3s] [%10s] [%-10s]\n", "abcdef", "right", "left");
printf("[%*d] [%.*f]\n", 6, 7, 2, 1.23456);
}
Fixed width integers
#include <stdio.h>
#include <inttypes.h>
int main(void) {
int64_t big = -9000000000;
uint32_t mask = 0xDEADBEEF;
size_t n = sizeof big;
printf("%" PRId64 " %" PRIx32 " %zu\n", big, mask, n);
}
stdout, stderr and buffering
fprintf(stderr, "error: %s\n", msg); // unbuffered: shows up immediately
printf("progress..."); // line buffered on a terminal, fully buffered in a pipe
fflush(stdout); // push it out now
setvbuf(stdout, NULL, _IONBF, 0); // turn buffering off
Reading Input
Read a line safely with fgets
char line[256];
if (fgets(line, sizeof line, stdin)) {
line[strcspn(line, "\n")] = '\0'; // remove the newline, if any
printf("you typed: %s\n", line);
}
scanf and its return value
int age;
double weight;
if (scanf("%d %lf", &age, &weight) != 2) {
fprintf(stderr, "expected a number and a weight\n");
return 1;
}
Parse a line with sscanf
#include <stdio.h>
int main(void) {
const char *line = "2026-09-29 temp=21.5 unit=C";
int y, m, d;
double t;
char unit[8];
int got = sscanf(line, "%d-%d-%d temp=%lf unit=%7s", &y, &m, &d, &t, unit);
printf("%d fields: %d/%d/%d %.1f %s\n", got, d, m, y, t, unit);
}
Read characters until EOF
int c; // int, not char, so EOF fits
while ((c = getchar()) != EOF) {
putchar(toupper(c));
}
File I/O
fopen modes, reading and writing text and binary files, errors and seeking
Open and Close
fopen modes
| Mode | Opens for | If the file exists | If missing |
|---|---|---|---|
"r" | reading | read from the start | fails |
"w" | writing | truncated to zero | created |
"a" | appending | writes go to the end | created |
"r+" | reading and writing | kept | fails |
"w+" | reading and writing | truncated | created |
"wx" | writing, exclusive (C11) | fails | created |
add b | binary, no newline translation on Windows |
Always check fopen
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main(void) {
FILE *f = fopen("missing.txt", "r");
if (!f) {
fprintf(stderr, "cannot open missing.txt: %s\n", strerror(errno));
return 1;
}
fclose(f);
}
Text Files
Read line by line
FILE *f = fopen("data.txt", "r");
if (!f) { perror("data.txt"); return 1; }
char line[512];
int n = 0;
while (fgets(line, sizeof line, f)) {
n++;
}
if (ferror(f)) perror("read");
fclose(f);
Write and append
FILE *log = fopen("app.log", "a");
if (log) {
fprintf(log, "%s started\n", name);
fclose(log);
}
Lines of any length
char *line = NULL;
size_t cap = 0;
ssize_t len;
while ((len = getline(&line, &cap, f)) != -1) {
process(line, (size_t)len);
}
free(line);
Binary Files
fread and fwrite
struct record { int32_t id; double value; };
struct record rs[100];
FILE *f = fopen("records.bin", "wb");
size_t written = fwrite(rs, sizeof rs[0], 100, f);
fclose(f);
f = fopen("records.bin", "rb");
size_t got = fread(rs, sizeof rs[0], 100, f);
fclose(f);
File size and seeking
fseek(f, 0, SEEK_END); long size = ftell(f); rewind(f); fseek(f, 128, SEEK_SET);
Read a whole file into memory
char *slurp(const char *path, size_t *out_len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
char *buf = NULL;
size_t len = 0, cap = 0, n;
char chunk[8192];
while ((n = fread(chunk, 1, sizeof chunk, f)) > 0) {
if (len + n + 1 > cap) {
size_t ncap = cap ? cap * 2 : 16384;
while (ncap < len + n + 1) ncap *= 2;
char *tmp = realloc(buf, ncap);
if (!tmp) { free(buf); fclose(f); return NULL; }
buf = tmp; cap = ncap;
}
memcpy(buf + len, chunk, n);
len += n;
}
fclose(f);
if (buf) buf[len] = '\0';
*out_len = len;
return buf;
}
Files in Practice
Write, then read back
#include <stdio.h>
int main(void) {
FILE *f = tmpfile(); // a temporary file, removed at close
if (!f) { perror("tmpfile"); return 1; }
for (int i = 1; i <= 3; i++) fprintf(f, "line %d\n", i);
rewind(f);
char buf[64];
int n = 0;
while (fgets(buf, sizeof buf, f)) { n++; fputs(buf, stdout); }
printf("%d lines, error=%d, eof=%d\n", n, ferror(f) != 0, feof(f) != 0);
fclose(f);
}
stdlib & Conversions
strtol, strtod, errno, random numbers, sorting, searching and environment
String to Number
strtol with full error checking
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
int parse_int(const char *s, int *out) {
char *end;
errno = 0;
long v = strtol(s, &end, 10);
if (end == s || *end != '\0') return -1; // no digits, or trailing junk
if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return -2;
*out = (int)v;
return 0;
}
int main(void) {
const char *tests[] = {"42", "12abc", "", "99999999999"};
for (int i = 0; i < 4; i++) {
int v = 0;
int rc = parse_int(tests[i], &v);
printf("\"%s\" -> rc %d, value %d\n", tests[i], rc, v);
}
}
Other conversions
double d = strtod("3.5e2", NULL);
unsigned long long big = strtoull("18446744073709551615", NULL, 10);
long hex = strtol("ff", NULL, 16);
long any = strtol("0x1F", NULL, 0); // base 0: detects 0x and leading 0
char buf[32];
snprintf(buf, sizeof buf, "%d", 42); // number to string
Random Numbers
rand, and better options
srand((unsigned)time(NULL)); // seed once int die = rand() % 6 + 1; // 1..6, fine for games arc4random_uniform(6); // BSD, macOS, glibc 2.36+: good quality, no seeding getrandom(buf, sizeof buf, 0); // Linux: bytes for keys and tokens
Sorting, Searching, Math Helpers
bsearch on a sorted array
int key = 23;
int *found = bsearch(&key, sorted, n, sizeof sorted[0], by_value);
if (found) printf("at index %td\n", found - sorted);
Integer helpers
#include <stdio.h>
#include <stdlib.h>
int main(void) {
div_t r = div(17, 5);
printf("%d %d %d %ld\n", r.quot, r.rem, abs(-9), labs(-9L));
}
Environment and processes
const char *home = getenv("HOME"); // NULL if not set
if (!home) home = "/tmp";
int status = system("ls -l"); // runs /bin/sh -c; avoid with user input
exit(EXIT_FAILURE);
Environment and Searching
bsearch on a sorted array
#include <stdio.h>
#include <stdlib.h>
static int cmp(const void *a, const void *b) { int x = *(const int *)a, y = *(const int *)b; return (x > y) - (x < y); }
int main(void) {
int v[] = {3, 8, 15, 23, 42, 57};
int key = 23;
int *hit = bsearch(&key, v, 6, sizeof v[0], cmp);
printf("%s at %td\n", hit ? "found" : "missing", hit ? hit - v : -1);
}
Math & Numbers
math.h functions, rounding, overflow checks, NaN and infinity, complex numbers
math.h
The everyday functions
#include <stdio.h>
#include <math.h>
int main(void) {
printf("%.4f %.1f %.4f %.4f\n", sqrt(2.0), pow(2, 10), exp(1.0), log(100) / log(10));
printf("%.1f %.1f %.1f %.1f\n", floor(-2.5), ceil(-2.5), round(-2.5), trunc(-2.5));
printf("%.4f %.1f %.4f\n", atan2(1, 1), fmod(10.5, 3), hypot(3, 4) - 5);
}
NaN and infinity
#include <stdio.h>
#include <math.h>
int main(void) {
double z = 0.0;
double inf = 1.0 / z, nan_v = z / z;
printf("%f %f\n", inf, nan_v);
printf("%d %d %d\n", isinf(inf), isnan(nan_v), nan_v == nan_v);
}
Overflow Checks
Checked arithmetic
#include <stdio.h>
#include <stdckdint.h>
#include <limits.h>
int main(void) {
int r;
if (ckd_add(&r, INT_MAX, 1)) puts("add overflowed");
size_t bytes;
if (!ckd_mul(&bytes, (size_t)1000, sizeof(double))) printf("%zu bytes\n", bytes);
}
Complex numbers
#include <stdio.h>
#include <complex.h>
int main(void) {
double complex z = 1.0 + 2.0 * I;
double complex w = z * conj(z);
printf("%.1f%+.1fi |z| = %.4f\n", creal(w), cimag(w), cabs(z));
}
Time & Dates
time, clock, struct tm, strftime, elapsed time and sleeping
Current Time
Format a timestamp
#include <stdio.h>
#include <time.h>
int main(void) {
time_t now = 1790690712; // a fixed moment: 2026-09-29 14:05:12 UTC
struct tm utc;
gmtime_r(&now, &utc);
char buf[64];
strftime(buf, sizeof buf, "%Y-%m-%dT%H:%M:%SZ", &utc);
puts(buf);
strftime(buf, sizeof buf, "%A %d %B %Y", &utc);
puts(buf);
}
struct tm fields
| Field | Range | Note |
|---|---|---|
tm_year | years since 1900 | 2026 is 126 |
tm_mon | 0 to 11 | January is 0 |
tm_mday | 1 to 31 | |
tm_hour tm_min tm_sec | 0-23, 0-59, 0-60 | 60 for a leap second |
tm_wday | 0 to 6 | Sunday is 0 |
tm_yday | 0 to 365 | |
tm_isdst | positive, 0, negative | negative lets mktime decide |
Measuring Time
Elapsed wall time with a monotonic clock
struct timespec t0, t1; clock_gettime(CLOCK_MONOTONIC, &t0); work(); clock_gettime(CLOCK_MONOTONIC, &t1); double ms = (t1.tv_sec - t0.tv_sec) * 1e3 + (t1.tv_nsec - t0.tv_nsec) / 1e6;
CPU time with clock()
clock_t start = clock(); work(); double secs = (double)(clock() - start) / CLOCKS_PER_SEC;
Date arithmetic with mktime
struct tm t = {.tm_year = 2026 - 1900, .tm_mon = 0, .tm_mday = 31, .tm_isdst = -1};
t.tm_mday += 30; // out of range on purpose
time_t when = mktime(&t); // normalizes to March 2, 2026
double days = difftime(when, start) / 86400.0;
Durations
Days between two dates
#include <stdio.h>
#include <time.h>
int main(void) {
struct tm a = {.tm_year = 2026 - 1900, .tm_mon = 0, .tm_mday = 1, .tm_hour = 12, .tm_isdst = -1};
struct tm b = {.tm_year = 2026 - 1900, .tm_mon = 8, .tm_mday = 29, .tm_hour = 12, .tm_isdst = -1};
double days = difftime(mktime(&b), mktime(&a)) / 86400.0;
printf("%.0f days\n", days);
}
C23 Features
nullptr, bool, auto, typeof, constexpr, attributes, checked arithmetic and more
The Headline Changes
A C23 program
#include <stdio.h>
#include <stdbit.h>
constexpr int LIMIT = 1'000;
[[nodiscard]] static int clamp(int v) { return v > LIMIT ? LIMIT : v; }
int main(void) {
auto x = clamp(0b1111'1111'1111); // auto infers int
typeof(x) y = x / 2;
bool ok = true;
int *p = nullptr;
static_assert(sizeof(int) >= 4); // the message is optional now
printf("%d %d %d %d %u\n", x, y, ok, p == nullptr, stdc_count_ones(255u));
}
What C23 added
| Feature | Example |
|---|---|
nullptr and nullptr_t | int *p = nullptr; |
bool keywords | bool ok = true; |
| type inference | auto n = 42; |
typeof, typeof_unqual | typeof(x) copy = x; |
constexpr objects | constexpr int N = 64; |
| standard attributes | [[nodiscard]] [[maybe_unused]] [[deprecated]] [[fallthrough]] [[noreturn]] |
| binary literals, digit separators | 0b1010, 1'000'000 |
| empty initializer | struct s x = {}; |
#embed, #elifdef, __has_include, __VA_OPT__ | preprocessor |
<stdckdint.h> | ckd_add, ckd_sub, ckd_mul |
<stdbit.h> | stdc_count_ones, stdc_bit_width, stdc_leading_zeros |
_BitInt(N) | exact width integers |
| enums with a fixed type | enum e : uint8_t { A, B }; |
unreachable() | in <stddef.h> |
memset_explicit | zeroing that is not optimized away |
strdup, strndup, memccpy | now in <string.h> |
%b in printf | binary output |
Attributes
The standard attributes
[[nodiscard("check the error code")]] int save(const char *path);
[[deprecated("use save() instead")]] int write_file(const char *path);
void handler(int sig, [[maybe_unused]] void *ctx);
switch (x) { case 1: work(); [[fallthrough]]; case 2: more(); }
[[unsequenced]] int square(int x); // no side effects, result depends only on x
Test for a feature
#if __STDC_VERSION__ >= 202311L
/* C23 */
#endif
#if __has_c_attribute(nodiscard)
/* the attribute is known */
#endif
Smaller Additions
unreachable and memset_explicit
#include <stddef.h>
#include <string.h>
int weekday_len(int d) {
switch (d) {
case 0: return 6;
case 1: return 7;
default: unreachable(); // tells the optimizer this cannot happen
}
}
void wipe_key(unsigned char *key, size_t n) {
memset_explicit(key, 0, n); // not removed as a dead store
}
stdbit.h
#include <stdio.h>
#include <stdbit.h>
int main(void) {
unsigned x = 40;
printf("%u %u %u %u\n", stdc_count_ones(x), stdc_trailing_zeros(x), stdc_bit_width(x), stdc_bit_ceil(x));
printf("%d\n", stdc_has_single_bit(64u));
}
C2y, the Next Standard
Already in GCC and Clang with -std=c2y
outer: for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (grid[i][j] == 0) break outer; // named loops (GCC 15)
if (int fd = open(path, O_RDONLY); fd >= 0) { use(fd); } // if declarations (GCC 15)
switch (c) { case 'a' ... 'z': lower(); break; } // case ranges, standard in C2y
size_t n = countof(table); // <stdcountof.h>, GCC 16, Clang 21
_Generic & Macros
type-generic selection, generic macros, X macros and safe macro patterns
_Generic
Choose code by type
#include <stdio.h>
#define type_name(x) _Generic((x), \
int: "int", long: "long", double: "double", \
char *: "char *", const char *: "const char *", \
default: "something else")
int main(void) {
printf("%s %s %s %s\n", type_name(1), type_name(1L), type_name(1.0), type_name("hi"));
}
A type-generic print
#define print(x) _Generic((x), \
int: print_int, double: print_double, char *: print_str)(x)
void print_int(int v) { printf("%d\n", v); }
void print_double(double v) { printf("%g\n", v); }
void print_str(char *s) { puts(s); }
Macro Patterns
X macros: one list, several expansions
#include <stdio.h>
#define COLORS(X) X(RED) X(GREEN) X(BLUE)
#define AS_ENUM(name) name,
#define AS_STRING(name) #name,
enum color { COLORS(AS_ENUM) COLOR_COUNT };
static const char *color_names[] = { COLORS(AS_STRING) };
int main(void) {
for (int c = 0; c < COLOR_COUNT; c++) printf("%d=%s ", c, color_names[c]);
putchar('\n');
}
container_of
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))
struct node { int value; struct list_head link; };
struct node *n = container_of(link_ptr, struct node, link);
Swap with typeof
#define SWAP(a, b) do { typeof(a) tmp_ = (a); (a) = (b); (b) = tmp_; } while (0)
Generic Maximum
A type-safe max with _Generic
#include <stdio.h>
static inline int max_i(int a, int b) { return a > b ? a : b; }
static inline double max_d(double a, double b) { return a > b ? a : b; }
#define max(a, b) _Generic((a) + (b), int: max_i, double: max_d)(a, b)
int main(void) {
printf("%d %.1f\n", max(3, 9), max(2.5, 1.0));
}
Threads & Atomics
C11 threads, mutexes, condition variables, atomics and POSIX threads
C11 Threads
Start and join threads, with a mutex
#include <stdio.h>
#include <threads.h>
static int counter = 0;
static mtx_t lock;
static int worker(void *arg) {
int n = *(int *)arg;
for (int i = 0; i < n; i++) {
mtx_lock(&lock);
counter++;
mtx_unlock(&lock);
}
return 0;
}
int main(void) {
mtx_init(&lock, mtx_plain);
thrd_t t[4];
int n = 100000;
for (int i = 0; i < 4; i++) thrd_create(&t[i], worker, &n);
for (int i = 0; i < 4; i++) thrd_join(t[i], NULL);
printf("%d\n", counter);
mtx_destroy(&lock);
}
Condition variables
mtx_lock(&m); while (!ready) cnd_wait(&cv, &m); // always in a loop: wakeups can be spurious take_item(); mtx_unlock(&m); mtx_lock(&m); ready = true; cnd_signal(&cv); mtx_unlock(&m);
Atomics
Lock-free counters
#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>
static atomic_int hits = 0;
static int worker(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++) atomic_fetch_add_explicit(&hits, 1, memory_order_relaxed);
return 0;
}
int main(void) {
thrd_t t[4];
for (int i = 0; i < 4; i++) thrd_create(&t[i], worker, NULL);
for (int i = 0; i < 4; i++) thrd_join(t[i], NULL);
printf("%d\n", atomic_load(&hits));
}
Compare and swap
atomic_int state = 0;
int expected = 0;
if (atomic_compare_exchange_strong(&state, &expected, 1)) {
/* we moved it from 0 to 1 */
} else {
/* someone else did; expected now holds the current value */
}
POSIX Threads
pthreads, the most common API
#include <pthread.h>
void *work(void *arg) { return NULL; }
pthread_t th;
pthread_create(&th, NULL, work, NULL);
pthread_join(th, NULL);
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&m);
pthread_mutex_unlock(&m);
call_once
Run initialization exactly once
#include <stdio.h>
#include <threads.h>
static once_flag flag = ONCE_FLAG_INIT;
static int config = 0;
static void init(void) { config = 42; puts("init ran"); }
static int worker(void *arg) { (void)arg; call_once(&flag, init); return config; }
int main(void) {
thrd_t t[3];
for (int i = 0; i < 3; i++) thrd_create(&t[i], worker, NULL);
int res;
for (int i = 0; i < 3; i++) thrd_join(t[i], &res);
printf("config=%d\n", res);
}
Bit Manipulation
masks, set, clear, toggle, shifts, byte order and bit counting
Set, Clear, Test
The four operations
#include <stdio.h>
int main(void) {
unsigned flags = 0;
flags |= 1u << 3; // set bit 3
flags |= 1u << 0; // set bit 0
flags &= ~(1u << 0); // clear bit 0
flags ^= 1u << 5; // toggle bit 5
printf("0x%02x %d %d\n", flags, (flags >> 3) & 1u, (flags & (1u << 4)) != 0);
}
Masks and fields
#define RGB_R(c) (((c) >> 16) & 0xFFu) #define RGB_G(c) (((c) >> 8) & 0xFFu) #define RGB_B(c) ((c) & 0xFFu) uint32_t pack = (r << 16) | (g << 8) | b; uint32_t low4 = x & 0xFu; // lowest four bits uint32_t without_low = x & ~0xFu;
Tricks
bool is_pow2 = x && !(x & (x - 1)); unsigned lowest = x & -x; // lowest set bit (x unsigned) x &= x - 1; // clear the lowest set bit int bits = __builtin_popcount(x); // GCC and Clang; C23: stdc_count_ones(x)
Shifts and Byte Order
Shifting rules
| Expression | Result |
|---|---|
x << n with n greater or equal to the width | undefined behavior |
1 << 31 on a 32-bit int | undefined behavior: use 1u << 31 |
-8 >> 1 | implementation defined, -4 on every mainstream compiler |
u >> n on unsigned | fills with zeros |
| rotate left by n | shift left by n, OR with a right shift by 32 - n, for 0 < n < 32; C2y adds stdc_rotate_left |
Byte order
#include <stdio.h>
#include <stdint.h>
#include <string.h>
int main(void) {
uint32_t v = 0x11223344;
unsigned char b[4];
memcpy(b, &v, 4);
printf("%02x %02x %02x %02x\n", b[0], b[1], b[2], b[3]);
uint32_t be = ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3];
printf("0x%08x\n", be);
}
Bit Sets
A bit set for many flags
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
#define WORD_BITS (sizeof(uint64_t) * CHAR_BIT)
#define SET(bs, i) ((bs)[(i) / WORD_BITS] |= (uint64_t)1 << ((i) % WORD_BITS))
#define TEST(bs, i) (((bs)[(i) / WORD_BITS] >> ((i) % WORD_BITS)) & 1u)
int main(void) {
uint64_t seen[4] = {0}; // 256 flags
SET(seen, 3); SET(seen, 64); SET(seen, 200);
printf("%d %d %d %d\n", (int)TEST(seen, 3), (int)TEST(seen, 4), (int)TEST(seen, 64), (int)TEST(seen, 200));
}
Build Systems
Make, CMake, pkg-config, warnings in CI, static analysis and formatting
Make
A minimal Makefile
CC ?= cc CFLAGS ?= -std=c17 -Wall -Wextra -O2 app: main.o util.o $(CC) $(CFLAGS) $^ -o $@ %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f app *.o
Everyday make commands
make # build the first target make -j$(nproc) # build in parallel make clean make CFLAGS="-O0 -g" # override a variable make -n # print the commands without running them
CMake
Configure, build, test
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j ctest --test-dir build --output-on-failure cmake --install build --prefix /usr/local
Build type flags
| CMAKE_BUILD_TYPE | Flags with GCC |
|---|---|
Debug | -g |
Release | -O3 -DNDEBUG |
RelWithDebInfo | -O2 -g -DNDEBUG |
MinSizeRel | -Os -DNDEBUG |
Quality Tools
Format, analyze, lint
clang-format -i src/*.c include/*.h clang-tidy src/main.c -- -std=c17 -Iinclude cppcheck --enable=warning,performance,portability src/ gcc -fanalyzer -c main.c # GCC's static analyzer scan-build make # Clang's static analyzer
Find library flags with pkg-config
pkg-config --cflags --libs sqlite3 libcurl gcc main.c $(pkg-config --cflags --libs sqlite3) -o app
Libraries
Static and shared libraries
gcc -c -O2 util.c -o util.o ar rcs libutil.a util.o # static library gcc main.c -L. -lutil -o app # linked into app gcc -shared -fPIC -O2 util.c -o libutil.so # shared library gcc main.c -L. -lutil -Wl,-rpath,'$ORIGIN' -o app ldd ./app # which shared libraries it loads
Debugging & Sanitizers
gdb, lldb, AddressSanitizer, UBSan, Valgrind, assert and core dumps
Sanitizers First
AddressSanitizer and UBSan
gcc -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer main.c -o app ./app
What each sanitizer finds
| Flag | Finds | Cannot combine with |
|---|---|---|
-fsanitize=address | buffer overflows, use after free, double free, leaks | thread, memory |
-fsanitize=undefined | signed overflow, bad shifts, null dereference, misaligned access | nothing: combine with address |
-fsanitize=thread | data races between threads | address, memory |
-fsanitize=memory (Clang) | reads of uninitialized memory | address, thread |
-fsanitize=leak | leaks only, very low overhead | thread, memory |
gdb
The commands you need
gdb --args ./app input.txt (gdb) break parse_line # stop when parse_line is called (gdb) run (gdb) next # next line, stepping over calls (gdb) step # into the call (gdb) print *ptr # inspect values (gdb) print arr[0]@5 # five elements of an array (gdb) bt # backtrace: where am I, who called me (gdb) frame 2 # look at the caller's variables (gdb) watch total # stop when total changes (gdb) continue
Crash in production: core dumps
ulimit -c unlimited ./app # Segmentation fault (core dumped) gdb ./app core # or: coredumpctl gdb on systemd systems (gdb) bt full
Other Tools
Valgrind memcheck
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./app
assert and debug prints
#include <assert.h> assert(n > 0 && "n must be positive"); // aborts with file and line when false #ifndef NDEBUG #define DBG(fmt, ...) fprintf(stderr, "%s:%d: " fmt "\n", __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__) #else #define DBG(...) ((void)0) #endif
Undefined Behavior
the common undefined behaviors, what compilers do with them and how to avoid them
What Undefined Means
Three kinds of "it depends"
| Kind | Meaning | Example |
|---|---|---|
| undefined behavior | the standard places no requirements; anything can happen | signed overflow, out of bounds access |
| implementation defined | each compiler picks and documents one behavior | right shift of a negative int, sizeof(long) |
| unspecified | one of several allowed outcomes, not documented | order of evaluating function arguments |
The optimizer assumes it never happens
int check(int x) {
if (x + 1 < x) return 1; // "overflow check" on a signed int
return 0;
}
The Usual Suspects
Undefined behaviors you will actually meet
| Behavior | Safe alternative |
|---|---|
| signed integer overflow | check first, use unsigned, or ckd_add |
| reading past the end of an array | pass lengths; test with ASan |
using memory after free | set pointers to NULL; single owner |
| reading an uninitialized local | initialize at declaration |
| dereferencing a null pointer | check return values of malloc, fopen |
| shift by the width or more, or into the sign bit | unsigned types, check the count |
| modifying a string literal | copy into an array first |
| modifying a variable twice without a sequence point | one change per statement |
| accessing an object through an incompatible pointer type | memcpy or a union |
| returning the address of a local | return by value, or allocate |
| a non-void function ending without return, when the caller uses the value | return on every path; -Wreturn-type warns |
Strict aliasing
float f = 1.0f; uint32_t bad = *(uint32_t *)&f; // undefined: reads a float through a uint32_t * uint32_t good; memcpy(&good, &f, sizeof good); // defined, and compiles to one instruction
Catch it at run time
gcc -O2 -g -fsanitize=undefined -fno-sanitize-recover=undefined main.c -o app
Checks That Prevent It
Safe overflow checks without C23
#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
static bool add_ok(int a, int b) { return !((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)); }
int main(void) {
printf("%d %d\n", add_ok(INT_MAX, 1), add_ok(100, 200));
int r;
printf("%d\n", __builtin_add_overflow(INT_MAX, 1, &r));
}
Secure C
bounds, format strings, integer overflow, input validation, hardening flags and secret handling
Bounds and Formats
Dangerous functions and replacements
| Avoid | Why | Use instead |
|---|---|---|
gets | no length limit, removed in C11 | fgets |
strcpy, strcat | no destination size | snprintf, or length checks with memcpy |
sprintf | no destination size | snprintf |
scanf("%s") | no length limit | %99s with a width, or fgets |
strtok | hidden global state | strtok_r |
atoi, atof | no error reporting | strtol, strtod |
printf(user_input) | format string attack | printf("%s", user_input) |
system with user data | shell injection | execv family with an argument vector |
Allocation sizes that cannot overflow
if (count > SIZE_MAX / sizeof *items) return NULL; // check before multiplying items = malloc(count * sizeof *items); size_t total; if (ckd_mul(&total, count, sizeof *items)) return NULL; // C23
Hardening
Compiler and linker hardening
gcc -O2 -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection \
-fcf-protection -fPIE -pie -Wl,-z,relro,-z,now main.c -o app
checksec --file=./app
Wipe secrets
unsigned char key[32]; derive_key(key, sizeof key, password); use_key(key); memset_explicit(key, 0, sizeof key); // C23; explicit_bzero on glibc and BSD
The bounds checking interfaces (Annex K)
errno_t e = strcpy_s(dst, sizeof dst, src);
Input Validation
Validate lengths before copying
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
static int set_name(char *dst, size_t cap, const char *src) {
size_t len = strnlen(src, cap);
if (len == cap) return -1; // too long, would not fit with the terminator
memcpy(dst, src, len + 1);
return 0;
}
int main(void) {
char name[8];
printf("%d %d\n", set_name(name, sizeof name, "Ana"), set_name(name, sizeof name, "Alexandra"));
printf("%s\n", name);
}
Compiler Errors
the error and warning messages you will meet, and what fixes them
Compile Errors
The messages and the fixes
| Message | Cause and fix |
|---|---|
implicit declaration of function 'foo' | missing #include or prototype; an error since GCC 14 |
undefined reference to 'foo' | declared but not linked: add the .c file, the library (-lm), or fix the name |
multiple definition of 'x' | a variable defined in a header; define it once in a .c file, declare extern in the header |
incompatible pointer type | wrong pointer type passed; an error since GCC 14 |
conflicting types for 'foo' | the prototype and the definition differ |
expected ';' before '}' token | a missing semicolon on the previous line |
control reaches end of non-void function | a path without return |
format '%d' expects argument of type 'int' | wrong conversion; use %ld, %zu or the PRI macros |
'for' loop initial declarations are only allowed in C99 mode | a very old -std=c89 build |
storage size of 'x' isn't known | a struct used before its definition is visible |
GCC 14 turned several warnings into errors
int main(void) {
int *p = 42; // int-conversion: an error in GCC 14+
char *s = &p; // incompatible-pointer-types: an error
return abs(-1); // implicit-function-declaration: an error without <stdlib.h>
}
Run Time Failures
Crashes and what they usually mean
| Symptom | Usual cause |
|---|---|
Segmentation fault | null or dangling pointer, stack overflow, writing to a string literal |
Bus error | misaligned access on some CPUs, or a truncated memory mapped file |
free(): double free detected | freeing the same pointer twice |
free(): invalid pointer | freeing something not from malloc, or a pointer moved from the start |
stack smashing detected | a local array written past its end |
Floating point exception | integer division by zero, or INT_MIN / -1 |
| the program works in debug and fails with -O2 | undefined behavior; build with -fsanitize=address,undefined |
Warnings Worth Treating as Errors
What -Wall -Wextra reports, and why it matters
| Warning | Usually means |
|---|---|
-Wformat | a printf argument of the wrong type |
-Wuninitialized, -Wmaybe-uninitialized | reading a variable before it has a value |
-Wreturn-type | a path that falls off the end of a non-void function |
-Wsign-compare | signed and unsigned values compared: -1 becomes huge |
-Wimplicit-fallthrough | a missing break in a switch |
-Wshadow | an inner variable hides an outer one |
-Wconversion | an implicit conversion that can change the value |
-Wparentheses | an expression whose precedence is easy to misread |
A format warning and its fix
size_t n = strlen(s);
printf("%d\n", n); // warning: format '%d' expects 'int', but argument has type 'size_t'
printf("%zu\n", n); // correct
Standards & Compilers
C89 to C23, C2y, compiler support and C next to C++, Rust and Go
The Standards
The C standards and what each brought
| Standard | Year | Highlights |
|---|---|---|
| C89 / C90 | 1989 | the first standard: prototypes, void, const |
| C99 | 1999 | // comments, declarations anywhere, long long, stdint.h, bool, VLAs, designated initializers, inline |
| C11 | 2011 | _Generic, threads and atomics, static_assert, anonymous structs, aligned memory, removed gets |
| C17 | 2018 | defect fixes only, no new features |
| C23 | 2024 (ISO/IEC 9899:2024) | nullptr, bool keywords, auto, typeof, constexpr, attributes, #embed, checked arithmetic, _BitInt |
| C2y | expected around 2029 | _Countof, named loops, if declarations, case ranges |
Compiler support in September 2026
| Compiler | Current version | Default C mode | C23 status |
|---|---|---|---|
| GCC | 16.2 | gnu23 (since GCC 15) | complete for the language |
| Clang | 23.1 | gnu17 | nearly complete |
| MSVC | Visual Studio 2022 and later | legacy MSVC C unless /std:c11 or /std:c17 | partial with /std:clatest |
| TCC, SDCC, compilers for microcontrollers | varies | often C99 or C11 | varies |
C and Its Neighbors
When to choose C
| Question | C | C++ | Rust | Go |
|---|---|---|---|---|
| memory management | manual | RAII, smart pointers | ownership checked by the compiler | garbage collected |
| memory safety | none built in | partial | safe by default | safe |
| ABI and FFI | the lingua franca | complex | via C ABI | via cgo |
| runtime size | tiny, runs on 8-bit microcontrollers | small | small | a runtime of a few MB |
| typical use | kernels, embedded, libraries, interpreters | games, browsers, large systems | new systems code, CLI tools | servers, tooling |
Checking Your Setup
Which standard and compiler am I using
#include <stdio.h>
int main(void) {
printf("__STDC_VERSION__ = %ld\n", __STDC_VERSION__);
#if defined(__clang__)
printf("clang %d.%d\n", __clang_major__, __clang_minor__);
#elif defined(__GNUC__)
printf("gcc %d.%d\n", __GNUC__, __GNUC_MINOR__);
#elif defined(_MSC_VER)
printf("msvc %d\n", _MSC_VER);
#endif
}
What Is a C Cheat Sheet
A C cheat sheet is a one page reference to the syntax, types, operators, standard library functions and tooling of the C programming language, laid out so a programmer finds the right form in seconds instead of searching a book. People also call it a C programming cheat sheet or a C language reference card, and they mean the same mix of syntax, pointers, printf formats and compiler flags.
This C cheat sheet covers 30 sections and 209 snippets for C99, C11, C17 and C23, with a look at C2y. Every snippet that needs a newer standard carries a chip, and the standard picker in the toolbar hides what your compiler mode cannot build. Rows that depend on POSIX, GNU extensions or MSVC carry a chip too, and ISO mode hides them. The printed output of the example programs was produced by compiling and running them with GCC 16.2 on x86-64 Linux.
The toolbox at the top lints C code for the classic bugs, explains any declaration in plain English, checks printf and scanf formats against their arguments, shows how the compiler groups an expression, computes struct layout and padding for four data models, inspects numbers and bit patterns, and turns text into C string literals or byte arrays. The builders write compiler commands with the right warnings, sanitizers and hardening, Makefiles, CMakeLists.txt files, integer type declarations with their printf macros, and debugger sessions. Everything runs in your browser.
How It Differs From a Textbook
A textbook explains C from the ground up; this page shows the shortest correct form of each construct next to the mistake that usually comes with it, such as sizeof on an array parameter, a missing parenthesis in a macro, or signed overflow. It works well next to a linter and the compiler's own warnings. For the language built on top of C, see the C++ cheat sheet; for a memory safe systems language, the Rust cheat sheet.
What This C Cheat Sheet Covers
The 30 sections go from the first compile to shipping safe code, and each link jumps to that section above.
- Foundations: compiling and running C with the toolbox and builders, program structure and main, types, sizes and limits, operators and precedence, and control flow.
- Data: arrays, strings and string.h, pointers, malloc, calloc, realloc and free, and structs, unions and enums.
- Functions and code: functions, function pointers and callbacks, the preprocessor, storage duration and linkage, and const, volatile and restrict.
- Standard library: printf and scanf, file I/O, stdlib and conversions, math and overflow checks, and time and dates.
- Modern C: C23 features, _Generic and macro patterns, threads and atomics, and bit manipulation.
- Tools and safety: Make and CMake, gdb and sanitizers, undefined behavior, and secure C.
- Help: compiler errors and crashes, and standards and compilers.
C Syntax Quick Reference
The forms people look up most often, one line each. The sections above show each of them with a full example and the mistakes that come with them.
| Task | C |
|---|---|
| Program entry point | int main(int argc, char *argv[]) { return 0; } |
| Include a header | #include <stdio.h> |
| Print a line | printf("%d items\n", n); |
| Read a line | fgets(buf, sizeof buf, stdin); |
| Declare an array | int a[10] = {0}; |
| Array length | sizeof a / sizeof a[0] |
| Declare a pointer | int *p = &x; |
| Allocate an array | int *v = malloc(n * sizeof *v); |
| Free memory | free(v); v = NULL; |
| Copy a string safely | snprintf(dst, sizeof dst, "%s", src); |
| Compare strings | strcmp(a, b) == 0 |
| String length | strlen(s) |
| Define a struct type | typedef struct { int x, y; } Point; |
| Initialize a struct | Point p = {.x = 1, .y = 2}; |
| Member through a pointer | ptr->x |
| Enumeration | enum color { RED, GREEN, BLUE }; |
| Function pointer | int (*op)(int, int) = add; |
| Constant | constexpr int N = 64; (C23) or #define N 64 |
| Parse an integer | long v = strtol(s, &end, 10); |
| Open a file | FILE *f = fopen("data.txt", "r"); |
| Sort an array | qsort(a, n, sizeof a[0], cmp); |
| Set a bit | flags |= 1u << n; |
| Test a bit | (flags >> n) & 1u |
| Checked addition | if (ckd_add(&r, a, b)) { /* overflow */ } |
| Compile with warnings | gcc -std=c23 -Wall -Wextra -g main.c -o app |
| Find memory errors | gcc -fsanitize=address,undefined -g main.c |
printf Format Specifiers Cheat Sheet
The conversion specifiers most people need, with the argument type each one expects. Passing the wrong type is undefined behavior, which is why GCC and Clang check formats with -Wformat, part of -Wall.
| Type | printf | scanf |
|---|---|---|
int | %d | %d |
unsigned int | %u, %x | %u |
long | %ld | %ld |
long long | %lld | %lld |
size_t | %zu | %zu |
int64_t | "%" PRId64 | "%" SCNd64 |
float | %f (promoted to double) | %f |
double | %f, %g, %e | %lf |
long double | %Lf | %Lf |
char | %c | " %c" |
| string | %s | %99s with a width |
| pointer | %p with (void *) | %p |
What C Is
C is a general purpose, compiled, statically typed programming language that maps closely to the machine: variables are named memory, pointers are addresses, and a struct is a block of bytes with a known layout. Dennis Ritchie created it at Bell Labs between 1969 and 1973 to rewrite the Unix operating system, and Brian Kernighan and Ritchie's book The C Programming Language (1978) spread it everywhere.
The language is standardized by ISO working group WG14. C23, published as ISO/IEC 9899:2024, is the current standard, and the next revision, called C2y for now, is in progress. GCC 16.2 and Clang 23.1 are the current releases of the two main open source compilers, and GCC compiles C23 by default since version 15.
C runs almost everywhere: operating system kernels such as Linux and Windows NT, embedded firmware on microcontrollers, databases such as SQLite, interpreters for Python and Lua, network servers, and the libraries every other language calls through its foreign function interface.
Where It Fits
Choose C when you need full control of memory and layout, a tiny runtime, maximum portability to unusual hardware, or a stable ABI for a library. For large applications, C++ or Rust usually trade a little of that control for safety and abstraction, and higher level languages such as Go or Python save development time. Much of C's reputation for danger comes from undefined behavior, which the sanitizers and warnings on this page catch early, part of standard software development best practices for any C project.
How a C Program Is Built
Every C source file goes through the same stages on its way to an executable, and knowing them explains most compiler and linker errors.
- The preprocessor handles every line starting with #: it pastes in headers, expands macros and drops code inside false #if blocks. gcc -E shows the result.
- The compiler parses the preprocessed translation unit, checks types and declarations, reports most errors and warnings, and optimizes the code.
- It emits assembly, and the assembler turns that into an object file with machine code and a table of the symbols it defines and needs.
- The linker combines the object files and libraries, matches every needed symbol to exactly one definition, and writes the executable. Missing definitions give undefined reference errors; duplicates give multiple definition errors.
- At run time, the loader maps the program into memory, resolves shared libraries, sets up the stack and calls main with argc and argv.
When a program fails, place the failure in one of these stages first: a syntax error is step 2, an undefined reference is step 4, and a crash is run time, where the debugging section helps.
C, C++ and Rust Compared
The differences that matter when choosing a systems language.
| Question | C | C++ | Rust |
|---|---|---|---|
| Memory management | manual malloc and free | RAII, smart pointers | ownership checked at compile time |
| Generic code | macros, _Generic, void pointers | templates | generics and traits |
| Error handling | return codes, errno | exceptions or return values | Result and Option |
| Standard library | small | large | large, plus crates |
| Compile times | fast | slow for heavy templates | moderate to slow |
| Binary interface | stable, the common ABI | compiler specific | unstable, uses the C ABI for FFI |
Most projects that call C from other languages go through the C ABI, which is why even new libraries often expose a C interface. The Python cheat sheet covers the most common host language for C extensions.
Common C Mistakes
The same handful of bugs shows up in most C code, from homework to production.
In Code
- Buffer overflows from strcpy, sprintf, gets or scanf("%s") without a width.
- Off by one errors: forgetting the '\0' of a string, or looping to i <= n.
- sizeof on an array parameter, which gives the size of a pointer.
- Using a pointer after free, or freeing it twice.
- Signed integer overflow, and mixing signed and unsigned values in comparisons.
- Unparenthesized macros, and macro arguments with side effects evaluated twice.
In Builds and Tooling
- Compiling without -Wall -Wextra and never reading the warnings.
- Never running the tests under -fsanitize=address,undefined.
- Relying on a platform's sizes: long is 8 bytes on Linux and 4 on Windows.
- Defining variables in headers, which gives multiple definition errors.
- Ignoring return values of malloc, fopen, fread and scanf.
- Depending on the evaluation order of function arguments.
How to Learn C Well
Start With Small Programs and Strict Flags
Write small command line programs that read input and print results, and compile every one with -Wall -Wextra -fsanitize=address,undefined from the first day. Learn pointers by drawing memory: every variable a box, every pointer an arrow. Use the Bash cheat sheet for the shell commands around your builds, and keep your exercises in version control with the git cheat sheet.
Then Learn the Machine and the Tools
Read the assembly of small functions on Compiler Explorer, step through programs in gdb, and study undefined behavior until you can spot it in review. An editor with clangd, set up with the VS Code workflow cheat sheet, shows errors as you type. Then read well written C, such as SQLite, Lua or the Linux kernel's lib/ directory.
How This C Cheat Sheet Is Maintained
This page is written and maintained by Bogdan Sandu for TMS Outsource, a software development company. It was checked against the C23 standard and the release notes of GCC 15, GCC 16 and Clang 23 on September 29, 2026, and the output of every example program on the page was produced by compiling and running it with GCC 16.2. If a snippet is wrong or out of date, the date in the byline shows when it was last reviewed.
C Programming Questions People Actually Ask
The questions developers search for most about C and about this cheat sheet, answered in a few sentences each.
Is C still worth learning?
Yes. C runs operating system kernels, embedded devices, databases, language runtimes and the libraries every other language calls, and it is the clearest way to learn how memory, pointers and the machine actually work. That knowledge makes you better at C++, Rust, Go and even Python, where performance problems usually come down to the same memory behavior.
What is the latest version of C?
C23, published as ISO/IEC 9899:2024, is the current standard. It added nullptr, bool as a keyword, auto type inference, typeof, constexpr objects, standard attributes, #embed, checked arithmetic in <stdckdint.h> and _BitInt. The next revision, known as C2y, is in progress, and GCC and Clang already implement parts of it with -std=c2y.
What is the difference between C and C++?
C++ started as C with classes and added templates, exceptions, references, overloading, namespaces and a large standard library. Most C code compiles as C++ with small changes, but the languages have diverged: C has designated initializers everywhere, variably modified types and _Generic, while C++ has RAII and templates. C is smaller and simpler; C++ offers more abstraction at the cost of complexity.
What does a pointer do in C?
A pointer holds the address of another object. With it a function can change the caller's variables, work with arrays and strings, manage memory from malloc, and build linked structures such as lists and trees. The * operator reads or writes the object at the address, and & takes the address of a variable.
How do I find memory leaks and crashes in C?
Compile with -g -fsanitize=address,undefined and run the program: AddressSanitizer reports buffer overflows, use after free, double free and leaks at the exact line. Valgrind finds similar problems without recompiling, and gdb shows where a crash happened with a backtrace. The debugger commands builder on this page writes the sessions for you.
What is undefined behavior in C?
Undefined behavior is any operation for which the standard places no requirements, such as signed integer overflow, reading past an array, dereferencing a null pointer or using freed memory. The program may crash, seem to work, or behave strangely only with optimization on, because compilers assume undefined behavior never happens and optimize on that basis.
Which compiler should I use for C?
GCC and Clang are both excellent, free and available on every major platform; GCC is the default on most Linux distributions and Clang on macOS. On Windows, use Clang or GCC through MSYS2, or MSVC for native Visual Studio projects, keeping in mind that MSVC's C support lags behind. Compile your code with two compilers when you can: each catches different mistakes.
What is the difference between malloc and calloc?
malloc(size) allocates uninitialized memory; calloc(count, size) allocates memory for count elements and sets every byte to zero, and it returns NULL instead of overflowing when count times size is too large. Both must be released with free, and both return NULL when memory runs out.
Is this C cheat sheet up to date?
Yes. It covers C99, C11, C17 and C23 with a look at C2y, and was checked against the C23 standard and the GCC 16.2 and Clang 23.1 releases on September 29, 2026. Every snippet that needs a newer standard carries a chip, the standard picker hides what your compiler mode cannot build, ISO mode hides POSIX, GNU and MSVC only rows, and the example output was produced by a real compiler.
Can I download this C cheat sheet as a PDF?
Yes. The page has a print stylesheet, so your browser's print command with Save as PDF produces a clean copy: the dark theme switches to a print palette, every output and explanation is expanded, and the search box, navigation and footer are left out. The print core button prints only the core snippets for a short reference, and you can collapse sections or run a search first to print only what is still visible.