Statements
21Choosing
if (n > 0)
puts("yes");
else
puts("no");The controlling expression is compared against 0; any scalar type works, pointers included.branch on a scalar conditionswitch (c) {
case 'a': f(); break;
default: g();
}The controlling expression is integer-promoted. Only integer constant expressions may be case labels.branch on an integer expressioncase 1:
case 2: f(); break;
default: g();Duplicate case values are a constraint violation. A case label outside any switch is an error; one nested inside an inner block still belongs to the switch. [undefined, unspecified or implementation-defined]a label inside a switchcase 'a' ... 'z':
lower++;
break;A GNU extension, not ISO C. Note the spaces around the dots.a range of case valuesLooping
while (*p)
p++;The test happens before the first iteration, so the body may run zero times.test, then bodydo {
n /= 10;
} while (n);Runs at least once. The trailing semicolon is required.body, then testfor (int i = 0; i < n; i++)
sum += a[i];Since C99 the first clause may be a declaration, and that object is scoped to the loop.init, test, advancefor (;;) {
if (done) break;
}It leaves ONE construct. To leave two nested loops you need a goto or a flag.leave the innermost loop or switchfor (i = 0; i < n; i++) {
if (!a[i]) continue;
use(a[i]);
}In a for loop the advance clause still runs; in a while loop it does not, which is how infinite loops get written.skip to the next iterationJumping
if (err) goto cleanup;
...
cleanup:
free(p);Labels are function-scoped. Jumping INTO the scope of a variable-length array is undefined; jumping forward past an initialisation leaves the object indeterminate.jump to a label in this functionreturn n < 0 ? -n : n;Falling off the end of a non-void function and then using the value is undefined behaviour. main is the one exception: it returns 0.leave the function, with a valueagain:
if (retry()) goto again;C23 allows a label at the end of a compound statement; before C23 it had to be followed by a statement.a named point in the functionvoid *t[] = {&&add, &&sub};
goto *t[op];A GNU extension: &&label yields a void*. How fast interpreter dispatch loops are written. [a common extension, not ISO C]computed gotoBlocks and expressions
{
int t = a; a = b; b = t;
}Declarations inside it are visible only within it, and automatic objects in it are destroyed at the closing brace.a compound statement, and a scopex = f(y);
puts("hi");The value is discarded. Discarding the result of a function marked warn_unused_result is what -Wunused-result catches.an expression statementwhile (*d++ = *s++)
;A whole loop body, when the work is in the header. Write it on its own line or it reads as a bug.the null statement#define MAX(a,b) ({ \
typeof(a) _a=(a), _b=(b); \
_a > _b ? _a : _b; })A GNU extension: the value of the last expression is the value of the block. The safe way to write a MAX macro. [a common extension, not ISO C]a statement expressionDeclarations, which are statements too
for (int i = 0; i < n; i++) { }Legal anywhere a statement is since C99. Before that, only at the top of a block.a declaration with an initialiserstatic_assert(sizeof(int) == 4,
"need 32-bit int");A keyword in C23; _Static_assert with before it. The message is optional since C23. [new in C23]a compile-time checktypeof(*p) tmp = *p;Standard in C23; __typeof__ is the GNU spelling that works everywhere. The operand is not evaluated. [new in C23]the type of an expressionconstexpr int N = 64;
int buf[N];C23. Unlike a const object, it may be used where a constant expression is required. [new in C23]a true compile-time constant objectKeywords
25Types
void *p = malloc(n);
void f(void);void* converts to and from any object pointer without a cast. f(void) means no parameters — and since C23, f() means the same.no value; or an incomplete typechar c = getchar();
unsigned char b = 0xFF;Exactly 1 byte by definition, but plain char is signed on x86 and unsigned on Arm. Say signed char or unsigned char when it matters.the smallest addressable unitlong n = 1L;
long long big = 1LL;Only the minimum widths are fixed: short and int at least 16 bits, long at least 32, long long at least 64.the signed integer typesunsigned u = 0u - 1u; /* UINT_MAX */Unsigned arithmetic wraps and is defined; signed overflow is undefined behaviour.the signedness qualifiersdouble x = 0.1 + 0.2; /* != 0.3 */long double is 80-bit on x86 Linux and plain double on AArch64 macOS — never assume.the floating typesbool ok = (p != NULL);Real keywords in C23; before that _Bool plus . Any nonzero scalar converts to true. [new in C23]the boolean type_BitInt(24) sample = 0;C23. Useful for hardware registers and bit-exact arithmetic; not subject to integer promotion in the usual way. [new in C23]an integer of exactly N bits#include <complex.h>
double complex z = 1 + 2*I;With , spelled complex and I. _Imaginary is optional and rarely implemented.complex arithmeticstruct P { int x, y; };
enum C { RED, GREEN };The tag lives in its own namespace, which is why struct foo and a variable foo can coexist.the aggregate and enumerated typeschar *p = nullptr;C23. Unlike NULL it has a type, so it cannot be passed as an int through varargs by mistake. [new in C23]the null pointer constantStorage and linkage
static int counter; /* file-local */
void f(void) { static int n; n++; }Two unrelated meanings: at file scope it hides the name from other translation units; inside a function it makes the object persist across calls. [undefined, unspecified or implementation-defined]internal linkage, or static storageextern int errno; /* header */
int errno; /* one .c */The declaration in a header, the definition in exactly one .c file. Without extern in the header you get multiple-definition errors.declare without definingauto n = 1 + 2; /* C23: int */The default, so it was noise for fifty years. In C23 it also means type inference, like C++.automatic storageregister int i; /* &i is an error */The hint is ignored by every modern compiler. The prohibition is real: you may not take the address of a register object.a hint, and a prohibitionthread_local int depth = 0;C23 spelling; _Thread_local in C11, __thread as the GNU extension before that. [new in C23]one instance per threadtypedef struct node Node;
typedef int (*cmp)(const void*, const void*);Not a macro: the name obeys scope, and typedef int a[10]; then const a x makes ten const ints, not a const pointer. [new in C23]a name for a typeinline int sq(int x) { return x*x; }
extern inline int sq(int); /* one .c */The linkage rule is the hard part: an inline function needs exactly one external definition somewhere, or the link fails at -O0. [new in C23]a suggestion, plus a linkage ruleQualifiers
const char *p; /* p may move */
char *const q; /* *q may change */A promise about the access path, not the object. Casting it away and then writing to a genuinely const object is undefined behaviour. [undefined, unspecified or implementation-defined]this lvalue may not be modified throughvolatile uint32_t *reg = (void*)0x4000;
*reg = 1;It stops the compiler eliding or reordering accesses. It says NOTHING about atomicity or about other threads — use _Atomic for those. [undefined, unspecified or implementation-defined]every access is an observable side effectvoid copy(int *restrict d,
const int *restrict s, int n);A promise you make to the optimiser. Break it — pass overlapping buffers to memcpy — and the behaviour is undefined, silently. [undefined, unspecified or implementation-defined]this pointer is the only route to the objectalignas(64) char line[64];
size_t a = alignof(double);C23 keywords; _Alignas/_Alignof plus in C11. [new in C23]alignment control and query_Atomic int n = 0;
n++; /* atomic RMW */With . Plain reads and writes of an _Atomic object are sequentially consistent.atomic access, with a memory orderOperators that are keywords
size_t n = sizeof arr / sizeof arr[0];Its operand is not evaluated — sizeof(a[i++]) does not increment i. The result is size_t, which is unsigned. [undefined, unspecified or implementation-defined]the size in bytes, at compile time#define abs(x) _Generic((x), \
int: abs, double: fabs)(x)C11. Type-based dispatch at compile time; the unselected branches are not even type-checked in C23. [new in C23]select an expression by typebool done = false;C23 keywords of type bool; macros expanding to 1 and 0 before that. [new in C23]the boolean constantsOperators & Precedence
27Highest — postfix
p->x == (*p).xa[i] is defined as *(a+i), which is why 3["abc"] compiles.subscript, call, member, member through pointerx = i++; /* x = old i */The value is the OLD one.postfix increment: use, then changef(&(struct P){1, 2});C99. It is an lvalue with automatic storage in a function, static at file scope.compound literalUnary
x = ++i; /* x = new i */The value is the NEW one.prefix: change, then use~0u == UINT_MAX
!x == (x == 0)Unary + is not a no-op: it applies the integer promotions.plus, negate, bitwise not, logical notint *p = &x;
*p = 5;Dereferencing a null, dangling or misaligned pointer is undefined behaviour, not a crash you can rely on. [undefined, unspecified or implementation-defined]dereference, address-of(double)n / dCasting a pointer to a wider-aligned type and dereferencing it is undefined even if the address happens to be aligned.castsizeof(int) alignof(max_align_t)Both are compile-time except sizeof of a VLA.size, alignmentBinary, in precedence order
7 / 2 == 3
-7 % 2 == -1Division or remainder by zero is undefined. INT_MIN / -1 overflows and is also undefined. [undefined, unspecified or implementation-defined]multiply, divide, remainderp + 1 /* next element */Pointer arithmetic is in units of the pointed-to type, and only within one object plus one past its end.add, subtract1u << 31
n >> 3Shifting by the width or more, or by a negative amount, is undefined. Right-shifting a negative value is implementation-defined. [undefined, unspecified or implementation-defined]shift left, shift rightif (a <= b) ...Comparing pointers into different objects is undefined; only equality is defined there.relationalif (p != NULL) ...Comparing a float with == is almost always a bug; compare against a tolerance.equalityif ((flags & MASK) != 0)Lower precedence than ==, which is the classic bug: (x & 1 == 0) parses as x & (1 == 0).bitwise anda ^= b; b ^= a; a ^= b;bitwise exclusive orflags |= O_APPEND;Also lower precedence than the comparisons. Parenthesise.bitwise orif (p && p->next)The right side is not evaluated if the left is 0, and there is a sequence point between them.logical and, short-circuitingif (!p || p->n == 0)Same, when the left is nonzero.logical or, short-circuitingmax = a > b ? a : b;Exactly one arm is evaluated. The common type of the arms is worked out by the usual conversions.conditionalAssignment and comma
x += 1;The left operand is evaluated only once in a compound assignment — the reason a[f()] += 1 is safe and a[f()] = a[f()] + 1 is not.assign and compound-assignflags &= ~MASK;bitwise compound assignmentfor (i = 0, j = n; i < j; i++, j--)Evaluates left, discards it, yields right. There IS a sequence point. Not the comma in an argument list.the comma operatorThe precedence traps
*p++ == *(p++)
(*p)++ /* different */Postfix binds tighter than unary *. (*p)++ increments what it points at. [undefined, unspecified or implementation-defined]increments p, not *p&a[3] == a + 3Subscript binds tighter than &.the address of the element(x & 1) == 0 /* what you meant */Equality binds tighter than the bitwise operators. This is the single most common C precedence bug.parses as x & (1 == 0)(unsigned)-1 >> 1 /* defined */Arithmetic shift on every real compiler, but the standard does not require it.implementation-defined(*p).f == p->fThe member operators bind tighter than the dereference. Use p->f.parses as *(p.f)Types & Limits
31What is actually guaranteed
The fixed-width types — <stdint.h>
<limits.h> and <float.h>
Conversion rules, in order
Declarations
23Reading one
int *a[10]; /* array of pointers */
int (*b)[10]; /* pointer to array */Right binds tighter: [] and () beat *. When in doubt, parenthesise, or run cdecl.start at the name, go right, then leftint (*fp)(int, int) = add;
int r = fp(1, 2);Without the parentheses it is a function returning int*.pointer to function returning intint *make(void);function returning pointer to intint main(int argc, char *argv[])As a parameter it is really char**; the array bound is discarded.array of pointers to charint (*ops[5])(void) = { f, g };The shape a dispatch table takes.array of 5 pointers to functiontypedef int (*op)(int, int);
op table[4];One typedef turns every declaration above into something readable.name the intermediate typeWhere const goes
const char *p = "hi";
p = "bye"; /* ok */
p[0] = 'H'; /* err */p may be reassigned; *p may not be written. Same as char const *p.pointer to const charchar *const p = buf;
p[0] = 'H'; /* ok */
p = other; /* err */p may not be reassigned; *p may be written.const pointer to charconst char *const msg = "hi";Read it right to left and it says exactly that.both*(char *)cp = 'x'; /* UB if const */Only if the underlying object was not itself defined const. [undefined, unspecified or implementation-defined]legal to do, undefined to then writeInitialisation
struct P p = {0};
int a[100] = {0};Any omitted member is zero-initialised, so {0} zeroes everything.zero-initialise the whole objectstruct P p = {};Same effect, and it works for a struct whose first member is itself a struct. [new in C23]the C23 empty initialiserstruct P p = { .y = 2 }; /* x = 0 */C99. Order-independent, and everything unnamed is zeroed.designated initialisersint a[] = { [9] = 1 }; /* 10 ints */The array is sized by the highest index used.designated array initialisersint x; /* garbage */
static int y; /* 0 */Reading it is undefined behaviour. Static and thread storage are zeroed for you. [undefined, unspecified or implementation-defined]the value is indeterminatechar a[] = "hi"; /* writable */
char *b = "hi"; /* not */Whereas char *s = "hi" points at a string literal you may not write to.a modifiable copy, 3 bytesStructs, unions, enums
The Preprocessor
23Directives
#include <stdio.h>
#include "config.h"The angle form searches only the system list, which is why a header of your own found with <> is a build-system bug.quoted searches locally first#define MAX(a,b) ((a) > (b) ? (a) : (b))A function-like macro is only that if the ( follows the name with no space.define and remove a macro#if defined(__linux__) && !defined(NDEBUG)The expression is integer only: no sizeof, no floats, no enums. Undefined identifiers become 0.conditional compilation#ifndef HDR_H
#define HDR_H
...
#endifThe include-guard idiom, and the wrong tool for anything with two conditions.shorthand for defined()#ifdef A
#elifdef B
#endifNew in C23; nothing else changed about conditionals. [new in C23]the C23 shorthands#if CHAR_BIT != 8
#error "need 8-bit bytes"
#endif#warning was a universal extension long before C23 standardised it.stop, or complain#pragma once
#pragma pack(push, 1)An unrecognised pragma is ignored, which is why -Wunknown-pragmas exists.implementation-defined instruction#line 42 "input.y"What generated code emits so errors point at the real source.reset the line number and file nameconst unsigned char logo[] = {
#embed "logo.png"
};C23. Replaces the xxd-and-generate-a-header dance entirely. [new in C23]include a binary file as bytesOperators inside a macro
#define STR_(x) #x
#define STR(x) STR_(x)
STR(__LINE__) /* "42" */It stringifies the argument BEFORE expansion, so you need a second level of macro to expand it first.stringify the argument#define CAT_(a,b) a##b
#define CAT(a,b) CAT_(a,b)Same two-level rule. The result must be a single valid token.paste two tokens together#define LOG(f, ...) \
printf(f, __VA_ARGS__)C99. At least one argument was required until C23.the variadic arguments#define LOG(f, ...) \
printf(f __VA_OPT__(,) __VA_ARGS__)C23 (a GNU extension long before). Solves the trailing-comma problem properly. [new in C23]x only if there were variadic args#define SWAP(a,b) do { \
int t=(a); (a)=(b); (b)=t; \
} while (0)Makes the macro safe as the body of an if without braces, and forces the trailing semicolon.the multi-statement macro wrapperPredefined, and required
printf("%s:%d\n", __FILE__, __LINE__);assert is built out of these two.the current file and lineprintf("in %s\n", __func__);C99. Not a macro — it is a static const char array the compiler declares.the enclosing function’s name#if __STDC_VERSION__ >= 202311L199901L, 201112L, 201710L, 202311L for C23.the standard, as a number#if __has_include(<threads.h>)C23, and a Clang extension for years before.is this header available?Standard Attributes
13C23 [[...]] attributes
[[deprecated("use g")]]
void f(void);Optional message: [[deprecated("use g()")]]. [new in C23]warn when this is used[[nodiscard]] int open_it(void);The standard spelling of GNU warn_unused_result. Optional message. [new in C23]warn when the result is thrown awayvoid cb([[maybe_unused]] void *ctx);For a parameter kept to match a callback signature. [new in C23]do not warn if this is unusedcase 1:
f();
[[fallthrough]];
case 2:A comment no longer counts under -Wimplicit-fallthrough=5. [new in C23]this switch case falls through deliberately[[noreturn]] void die(const char *m);Replaces C11’s _Noreturn, which is deprecated. [new in C23]this function does not return[[unsequenced]] int sq(int x);Lets the compiler cache the result across calls. [new in C23]pure: no state, same answer every timeThe GNU forms they replaced
void closep(FILE **f){ if(*f) fclose(*f); }
__attribute__((cleanup(closep)))
FILE *f = fopen(p, "r");Runs f when the variable leaves scope — the closest C gets to RAII, and still an extension. [a common extension, not ISO C]no standard equivalentUndefined Behaviour
28Arithmetic
if (ckd_add(&r, a, b)) overflow();Which is why `if (x + 1 < x)` is deleted. Use __builtin_add_overflow or the C23 ckd_* functions. [undefined, unspecified or implementation-defined]the optimiser assumes it cannot happenMemory and pointers
float f; uint32_t u;
memcpy(&u, &f, sizeof u);Accessing an object through a pointer to an unrelated type. memcpy is the portable fix; a union works in C. [undefined, unspecified or implementation-defined]undefinedSequencing and objects
What to do about it
Strings & Memory
32<string.h> — the memory half
<string.h> — the string half
<stdlib.h> — memory and conversion
Input & Output
40<stdio.h> — streams
printf conversions
Length modifiers and flags
scanf, and why not to
Maths & Time
24<math.h> — the ones with sharp edges
<time.h>
<ctype.h>, and the trap in it
isspace((unsigned char)*p)The argument must be representable as unsigned char or be EOF. Passing a plain char that is negative is undefined behaviour. [undefined, unspecified or implementation-defined]classify a character