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.
| Where | Examples |
|---|---|
| Operating systems | the Linux and BSD kernels, large parts of Windows and macOS |
| Embedded and firmware | microcontrollers (Arm Cortex-M, AVR, ESP32, RISC-V), where C is often the vendor's main supported language |
| Language runtimes | CPython, Lua, PHP, Ruby's reference interpreter |
| Infrastructure | SQLite, PostgreSQL, Redis, Git, curl, nginx |
| Glue between languages | the 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 |
| Standard | Name | What it added |
|---|---|---|
| ANSI 1989, ISO 1990 | C89 / C90 | function prototypes, const, void *, the standard library |
| ISO/IEC 9899:1999 | C99 | // comments, declarations anywhere, <stdint.h>, _Bool, long long, designated initializers, compound literals, variable-length arrays (VLAs), inline |
| ISO/IEC 9899:2011 | C11 | _Generic, _Static_assert, _Alignas, anonymous structs and unions, optional <threads.h> and <stdatomic.h>; gets removed, VLAs made optional |
| ISO/IEC 9899:2018 | C17 | defect fixes only |
| ISO/IEC 9899:2024 | C23 | nullptr, 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.
| Stage | Tool | Input → output | Errors from this stage |
|---|---|---|---|
| preprocess | cpp (gcc -E) | .c plus its headers → one translation unit (TU) | missing header, broken macro |
| compile | cc1 (gcc -S) | C → assembly (.s) | syntax and type errors, all warnings |
| assemble | as (gcc -c) | assembly → object file (.o): machine code plus a symbol table | almost never |
| link | ld (driven by gcc) | .o files + libraries → executable | undefined 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| Flag | Meaning |
|---|---|
-std=c23 | language version: c17, c11, c99; gnu23 also allows GNU extensions |
-Wall -Wextra | the common warnings (-Wall is not "all"); always on |
-pedantic | warn about anything that is not ISO C for the chosen -std |
-Werror | turn warnings into errors: use in CI |
-Wconversion, -Wshadow | stricter extras: implicit narrowing, shadowed names |
-O0 / -O2 / -O3 / -Os | optimization: none (default), standard, aggressive, small |
-g | debug info for gdb and lldb |
-fsanitize=address,undefined | AddressSanitizer + UndefinedBehaviorSanitizer: runtime checks, about 2× slower |
-I dir | add a directory to the header search path (#include "x.h") |
-L dir | add a directory to the library search path |
-l name | link libname.so or libname.a, e.g. -lm for <math.h> on Linux; put it after the files that use it |
-c | compile to an object file, do not link |
-o file | name the output (default a.out) |
-D NAME=value | define a macro; -DNDEBUG turns assert off |
-MMD -MP | write .d dependency files for make |
Reading compiler errors
This file has three mistakes on purpose:
#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'.
| Message | Usually means |
|---|---|
expected ';' before 'x' | the ; is missing at the end of the previous line |
'x' undeclared | a 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 type | e.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 function | a 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
#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| Piece | Meaning |
|---|---|
#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 main | same as exit(n): flushes output, runs atexit handlers; the shell sees n & 0xFF on POSIX |
no return in main | returns 0 (only main gets this) |
exit(n) / abort() | end the program from anywhere; abort skips cleanup and raises SIGABRT |
stdin, stdout, stderr | the three standard streams; diagnostics go to stderr |
// and /* */ | comments; /* */ does not nest |
Types
Integers
| Type | Standard minimum | x86-64 Linux, macOS | 64-bit Windows | printf |
|---|---|---|---|---|
char | 8 bits (CHAR_BIT) | 8 | 8 | %c, %hhd |
short | 16 | 16 | 16 | %hd |
int | 16 | 32 | 32 | %d |
long | 32 | 64 | 32 | %ld |
long long | 64 | 64 | 64 | %lld |
size_t | unsigned, 16 | 64 | 64 | %zu |
ptrdiff_t | signed | 64 | 64 | %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.
<stdint.h> | Meaning |
|---|---|
int8_t, int16_t, int32_t, int64_t | exactly that width, two's complement; uint8_t … uint64_t unsigned |
int_least16_t, int_fast32_t | at least that wide / the fastest type at least that wide |
intmax_t, uintmax_t | the widest integer types (%jd, %ju) |
intptr_t, uintptr_t | an integer that can hold a pointer |
INT32_MAX, UINT64_MAX, SIZE_MAX | limits (<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
| Type | In practice | printf / scanf | Literal |
|---|---|---|---|
float | IEEE 754 single, ≈7 significant digits | %f / %f | 1.5f |
double | IEEE 754 double, ≈15–16 digits: the default | %f, %g, %e / %lf | 1.5, 1e-9 |
long double | 80-bit extended on x86-64 Linux, same as double on MSVC and Apple Arm | %Lf | 1.5L |
bool | 0 or 1; C23 keyword (C99–C17: _Bool, or bool from <stdbool.h>) | %d | true, false |
char | one byte; signedness is implementation-defined: signed on x86, unsigned on Arm Linux | %c | 'a' |
0.1 + 0.2 == 0.3is false, as in every language with binary floating point. Compare with a tolerance.<math.h>(sqrt,pow,floor,isnan,INFINITY) needs-lmwhen linking with glibc.- Floating-point division by zero gives
infornan; integer division by zero is undefined behavior. - Any scalar converts to
bool: zero is false, everything else (including0.5and any non-null pointer) is true. But comparisons and!,&&,||give anint0 or 1, not abool. char,signed charandunsigned charare three distinct types. Useunsigned char(oruint8_t) for raw bytes and plaincharonly 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:
- If either operand is floating, the other converts to the wider floating type.
- Otherwise, after promotion, the operand of lower rank converts to the type of higher rank
(
int<long<long long). - 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.
| Expression | Result | Why |
|---|---|---|
7 / 2 | 3 | integer division truncates |
-7 / 2, -7 % 2 | -3, -1 | truncates toward zero; Python gives -4 and 1 |
7 / 2.0 | 3.5 | one double operand makes it floating |
-1 < 0u | 0 (false) | -1 converts to unsigned: 4294967295 |
(unsigned char)200 + (unsigned char)100 | 300 | both promote to int first |
uint8_t b = 200 + 100; | 44 | converting to an unsigned type wraps modulo 2ⁿ (256) |
UINT_MAX + 1u | 0 | unsigned arithmetic wraps, always |
INT_MAX + 1 | undefined behavior | signed overflow is UB, not wrap-around |
1 << 31 | undefined behavior (32-bit int) | the result does not fit; write 1u << 31 |
(int)3.99, (int)-3.99 | 3, -3 | floating → integer truncates; out of range is UB |
(int8_t)200 | -56 with GCC and Clang | out-of-range conversion to a signed type is implementation-defined |
strlen("") - 1 | SIZE_MAX | size_t is unsigned, so it wraps to a huge number |
sizeof 'a' | 4 | character constants have type int in C (char in C++) |
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).
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
| Literal | Type | Note |
|---|---|---|
42, 42u, 42l, 42ul, 42ll | int, unsigned, long, unsigned long, long long | an unsuffixed decimal gets the first signed type that fits |
0x2A, 052, 0b101010 | int | hex, octal (a leading zero!), binary (C23) |
1'000'000 | int | C23 digit separators |
3.14, 3.14f, 1e-9, 0x1p-4 | double, float, double, double | the last is a hex float, exactly 1/16 |
'a', '\n', '\0', '\x41' | int | single 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, wide | u8 literals are char8_t (unsigned char) arrays in C23 |
nullptr | nullptr_t | C23 null pointer constant |
true, false | bool | C23 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| Keyword | Meaning |
|---|---|
const | cannot 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 |
constexpr | C23, objects only (not functions): the value is fixed at compile time and usable anywhere a constant is needed (GCC 13+, Clang 19+) |
enum | named int constants; an enum variable accepts any integer, there is no checking |
enum E : T | C23: chooses the underlying integer type (GCC 13+, Clang 20+) |
#define | preprocessor 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.
| Expression | Parses as | Fix |
|---|---|---|
x & MASK == 0 | x & (MASK == 0) | (x & MASK) == 0: bitwise ops bind looser than == |
x << 1 + 1 | x << 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 == c | a = (b == c) | fine if intended, otherwise parenthesize |
if (x = 0) | assignment, always false | if (x == 0); -Wall warns |
i = i++ + 1 | undefined behavior | modify a variable once per expression |
f(next(), next()) | argument order is unspecified | call into locals first |
-7 >> 1 | implementation-defined (-4 on GCC/Clang) | shift unsigned values |
sizeof x + 1 | (sizeof x) + 1 | sizeof is an operator; parentheses needed only for types |
| Precedence (high → low) | Operators | Associativity |
|---|---|---|
| postfix | a() a[i] s.m p->m x++ x-- | left |
| unary | ! ~ -x +x ++x --x *p &x (type) sizeof alignof | right |
| multiplicative | * / % | left |
| additive | + - | left |
| shift | << >> | left |
| relational | < <= > >= | left |
| equality | == != | left |
| bitwise | &, then ^, then | | left |
| logical | &&, then || (short-circuit) | left |
| conditional | c ? a : b | right |
| assignment | = += -= *= /= %= <<= >>= &= ^= |= | right |
| comma | a, 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
}| Construct | Note |
|---|---|
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 |
switch | integer and character values only, no strings; case labels must be constant expressions |
| fallthrough | missing break runs the next case; GCC's -Wextra warns unless marked [[fallthrough]]; (Clang needs -Wimplicit-fallthrough) |
break, continue | innermost 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.
| Form | Meaning |
|---|---|
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 arguments | one 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/Makefileinclude/stack.h # the interface: types and prototypessrc/main.c # uses the stackstack.c # implements it#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#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;
}#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| Term | Meaning |
|---|---|
| translation unit | one .c file after the preprocessor has pasted in its headers |
| external linkage | the name is shared across translation units: functions and file-scope variables by default |
| internal linkage | static 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 scope | a 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 once | non-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);
}| Macro | Call | Expands to | Problem |
|---|---|---|---|
#define SQ(x) x * x | SQ(1 + 2) | 1 + 2 * 1 + 2 = 5 | parenthesize every parameter |
#define TWICE(x) (x) + (x) | 3 * TWICE(2) | 3 * (2) + (2) = 8 | parenthesize 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 conditional | wrap in do { … } while (0) |
ARRAY_LEN(p) | p is a pointer | sizeof p / sizeof *p | silently 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 macro | Use |
|---|---|
#if, #ifdef, #ifndef, #elif, #else, #endif | conditional 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_VER | platform 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 timeArrays 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==; usememcpyandmemcmp. - 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
sizeofdoes 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.
<string.h> | Does | Danger |
|---|---|---|
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 bytes | leaves dst unterminated when src has n or more bytes |
strcat(dst, src) | append | overflow, and O(n) to find the end |
strcmp(a, b) | negative, 0 or positive | a == b compares addresses, not text |
strncmp(a, b, n) | first n bytes only | |
strchr, strrchr, strstr | find a char (first, last) or substring | return a pointer into the string, or null |
strspn, strcspn | length of the prefix made of (or free of) given chars | s[strcspn(s, "\n")] = '\0' strips a newline |
strtok(s, ",") | split in place | writes '\0' into s, keeps hidden state: not reentrant |
memcpy, memmove, memset, memcmp | raw bytes, explicit length | memcpy with overlapping ranges is UB: use memmove |
strdup(s), strndup(s, n) | heap copy; caller must free | standard 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);
}| Feature | Note |
|---|---|
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 = value | designated 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->m | member of a struct, member through a pointer |
a = b | copies the whole struct; a == b does not compile: compare field by field |
| pass or return by value | copies; pass const T * for large structs |
unsigned ready : 1; | bit-field: a member of N bits; layout is implementation-defined |
| memory layout and padding | see 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
| Conversion | Argument | Example → output |
|---|---|---|
%d, %i | int (also char, short: they promote) | printf("%d", -5) → -5 |
%u | unsigned int | |
%ld, %lld, %lu, %llu | long, long long, unsigned versions | |
%zu, %td, %jd | size_t, ptrdiff_t, intmax_t | printf("%zu", sizeof(int)) → 4 |
"%" PRId64, PRIu32, PRIx64 | int64_t, uint32_t, uint64_t in hex | from <inttypes.h> |
%x, %X, %o, %b | unsigned in hex, HEX, octal, binary (%b is C23) | printf("%#x", 255) → 0xff |
%f, %.2f, %e, %g | double (float promotes) | printf("%.2f", 3.14159) → 3.14 |
%Lf | long double | |
%c | a character (as int) | |
%s, %.*s | a null-terminated string; at most N bytes of one | printf("%.*s", 3, "abcdef") → abc |
%p | a void * (cast other pointers) | printf("%p", (void *)p) → 0x7ffc… |
%% | a literal % | |
%5d, %-5d, %05d, %+d, %*d | width, left-align, zero-pad, always sign, width from an argument | printf("[%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:
| Caveat | Detail |
|---|---|
| check the return value | it is the number of items assigned, or EOF; if (scanf("%d", &n) != 1) |
| bad input stays in the stream | a failed %d leaves the non-digits unread: loop and you spin forever |
%s has no limit | write a width: char w[32]; scanf("%31s", w); |
| numeric overflow | an out-of-range number for %d is undefined behavior |
%d leaves the newline | a 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);
}| Mode | Opens for | If the file exists | If not |
|---|---|---|---|
"r" | reading | read from the start | fails |
"w" | writing | truncated to zero | created |
"a" | appending | writes go to the end | created |
"r+", "w+", "a+" | reading and writing | as above | as above |
"wx" | writing, exclusive (C11) | fails | created |
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/fwritemove raw bytes and return the number of items transferred.while (!feof(f))is a bug:feofonly turns true after a read has already failed. Loop on the read call's return value, then checkferror(f)to tell an error from end of file.stdoutis line-buffered on a terminal and fully buffered into a pipe or file;stderris 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.
| Convention | Example | Notes |
|---|---|---|
| null pointer | fopen, malloc, strchr | test with if (!p) |
-1 or negative | POSIX read, close; printf | 0 or positive is success |
EOF | getchar, fgetc, fscanf, fclose | EOF is a negative int; store getchar() in an int, not a char |
| count | fread, fwrite, scanf | fewer than requested means end of file or error |
bool + out-parameter | bool parse_int(const char *s, int *out) | keeps the error and the value separate |
| an error enum | typedef enum { OK, ERR_IO, ERR_PARSE } Err; | callers can switch on it |
errno | set by many library calls on failure | only 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);
}| Tool | Use |
|---|---|
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 call | needed for functions like strtol whose return value cannot signal an error |
ENOENT, EACCES, ENOMEM, EINVAL, ERANGE, EINTR | common 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
| Feature | Example | Before C23 |
|---|---|---|
nullptr, nullptr_t | int *p = nullptr; | NULL, which may be plain 0 |
bool, true, false keywords | bool ok = true; | <stdbool.h> macros |
typeof, typeof_unqual | typeof(x) tmp = x; | GNU __typeof__ |
auto type inference | auto n = strlen(s); (size_t) | none (auto was a useless storage class) |
constexpr objects | constexpr int N = 8; | enum or #define |
static_assert keyword, message optional | static_assert(sizeof(int) == 4); | _Static_assert(e, "msg") |
alignas, alignof, thread_local keywords | alignas(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 optimizer | GCC 15 supports them; Clang does not yet |
| empty initializer | struct s v = {}; (VLAs too) | = {0} |
#embed | binary file contents into an initializer | xxd -i |
#elifdef, #elifndef, #warning, __has_include, __VA_OPT__ | preprocessor additions | compiler extensions |
binary literals, %b | 0b1010, printf("%b", 10u) | GNU extension |
| digit separators | 1'000'000, 0xFF'FF | none |
enum E : uint8_t { … } | fixed underlying type | none |
_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, memccpy | now ISO C | POSIX only |
memset_explicit(p, 0, n) | a memset the optimizer may not remove, for wiping secrets | explicit_bzero (glibc, BSD); glibc has it from 2.43 |
int f() means int f(void) | old K&R-style definitions are removed | unknown arguments |
| unnamed parameters | void cb(int, void *ctx) | name them and cast to void |
| labels before declarations and at the end of a block | done: } | needed a ; |
| two's complement | signed integers are two's complement | ones' complement and sign-magnitude allowed in theory |
realloc(p, 0) | undefined behavior | implementation-defined |
__STDC_VERSION__ | 202311L | 201710L |
Undefined behavior
The standard sorts non-portable code into three kinds:
- Implementation-defined: each compiler picks and documents a behavior:
sizeof(long),charsignedness, 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 behavior | Example | Caught by |
|---|---|---|
| signed integer overflow | INT_MAX + 1, -INT_MIN, INT_MIN / -1 | UBSan, -ftrapv, ckd_add |
| integer division or remainder by zero | n / 0, n % 0 | UBSan |
| bad shift | 1 << 32 (32-bit int), x << -1, -1 << 1 | UBSan |
| out-of-bounds access | a[5] for int a[5], s[strlen(s) + 1] | ASan, -fsanitize=bounds |
| null pointer dereference | *p with p == nullptr | crash (usually), UBSan, -fanalyzer |
| use after free, double free | see Common memory bugs | ASan, Valgrind |
| reading an uninitialized local | int x; if (x) … | -Wall (-Wuninitialized), Valgrind, Clang's MemorySanitizer |
| modifying a string literal | char *s = "hi"; s[0] = 'H'; | crashes on most systems (literals are read-only) |
| unsequenced modification | i = i++;, a[i] = i++; | -Wall (-Wsequence-point, Clang -Wunsequenced) |
printf format mismatch | printf("%d", 3.0) | -Wall (-Wformat) |
| using the value of a function that fell off its end | missing return | -Wall (-Wreturn-type) |
| float → int out of range | (int)1e10 | UBSan (-fsanitize=float-cast-overflow) |
negative char to <ctype.h> | isdigit(c) with c < 0 | cast to unsigned char |
| incompatible pointer access (strict aliasing) | reading a float through an int * | memcpy; see aliasing |
| data race | two threads, one writing, no lock or atomic | ThreadSanitizer (-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.
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| Syntax | Meaning |
|---|---|
make, make app | build the first target, or a named one |
$@, $<, $^ | the target, the first prerequisite, all prerequisites |
%.o: %.c | a pattern rule for every object file |
CC, CFLAGS, LDFLAGS, LDLIBS | conventional variables (CC defaults to cc) |
.PHONY: clean | targets that are not files |
make -j8 | run up to 8 recipes in parallel |
make -n | print 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:
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/appComing from Python or TypeScript
| Concept | Python | TypeScript | C |
|---|---|---|---|
| run | python app.py | node app.js after tsc | compile and link to a native binary, then run it |
| types | dynamic | static, erased at runtime | static, and they decide memory layout |
| integers | unlimited size | number (a double), bigint | fixed width; unsigned wraps, signed overflow is UB |
x // y, x % y | floor division | Math.floor(x / y) | x / y truncates toward zero |
x ** y | built in | built in | pow(x, y) on double |
| strings | immutable Unicode str | immutable UTF-16 string | mutable char arrays ending in '\0'; you manage the space |
len(xs), xs.length | built in | built in | sizeof xs / sizeof xs[0] for arrays in scope; otherwise pass the length |
| lists | list | Array | fixed arrays; grow with realloc yourself |
| dicts, sets | built in | Map, Set | none in the standard library: write one or use a library |
| memory | garbage collected | garbage collected | malloc/free by hand (memory sheet) |
| errors | exceptions | exceptions | return values and errno |
| null | None | null, undefined | nullptr / NULL; dereferencing one is UB |
| out-of-range index | IndexError | undefined | undefined behavior |
| modules | import | import / export | #include a header, link the object files |
| visibility | _name convention | export | static hides a name inside its file |
| generics | duck typing | type parameters | void *, macros, _Generic |
| formatting | f-strings | template literals | printf, snprintf |
and, or, not | keywords | &&, ||, ! | &&, ||, !, giving int 0 or 1 |
| packages | pip | npm | system 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.
#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.
#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.
#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/Makefileinclude/stack.hsrc/main.cstack.cbuild/ # generated: objects and .d filesCFLAGS := -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#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:9ASan 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
- cppreference: C reference (opens in a new tab): the language and standard library, with the version each feature arrived in
- cppreference: C23 (opens in a new tab): every C23 change in one list
- N3220 working draft (opens in a new tab): the freely available draft closest to the published C23 standard
- GCC: Options to request or suppress warnings (opens in a new tab): what
-Wall,-Wextraand the rest enable - GCC: Options controlling C dialect (opens in a new tab):
-std=values and related flags - GCC 15 release notes (opens in a new tab): the switch to C23 by default
- Clang: C support status (opens in a new tab): which Clang version implements each C23 feature
- Clang: UndefinedBehaviorSanitizer (opens in a new tab): the checks UBSan can run
- SEI CERT C Coding Standard (opens in a new tab): rules for secure C, each with compliant and non-compliant examples
- Beej's Guide to C Programming (opens in a new tab): a free, friendly tutorial and library reference
- GNU make manual (opens in a new tab): rules, variables and functions
- Compiler Explorer (opens in a new tab): see the assembly any compiler produces for a snippet