C Core Idioms Cheatsheet
Language-level patterns. Minimal abstraction. Bounds and ownership made explicit.
Strings: index ↔ pointer
| Intent | Index style | Pointer style |
|---|---|---|
| Scan | while (s[i]) ++i; | while (*p) ++p; |
| Parallel scan | a[i], b[i] | *a++, *b++ |
| Bounded scan/output | i < cap | p < end |
| Copy | dst[i] = src[i] | *dst++ = *src++ |
| Copy + test NUL | if (!(dst[i] = src[i])) | if (!(*dst++ = *src++)) |
| Read + advance | s[i++] | *p++ |
| Write + advance | dst[i++] = c | *p++ = c |
| Distance | i | (size_t)(p - start) |
| Filter | read/write indexes | read/write pointers |
Bounded string copy
Index
size_t str_icopy(char *dst, size_t cap, const char *src) {
if (cap == 0) return 0;
for (size_t n = 0; n < cap; n++) {
dst[n] = src[n];
if (src[n] == '\0') return n;
}
dst[cap - 1] = '\0';
return cap - 1;
}
Bounded string copy
Pointer
size_t str_pcopy(char *dst, size_t cap, const char *src) {
if (cap == 0) return 0;
char *p = dst;
char *e = dst + cap;
while (p < e) {
if (!(*p++ = *src++))
return (size_t)(p - dst - 1);
}
*--p = '\0';
return cap - 1;
}
Arrays
| Intent | Index style | Pointer style |
|---|---|---|
| Traverse | for (i = 0; i < n; i++) | for (p = a; p < end; p++) |
| Push back | a[n++] = x; | *end++ = x; |
| Pop back | x = a[--n]; | x = *--end; |
| Unordered remove | a[i] = a[--n]; | *p = *--end; |
| Ordered remove | shift tail left | shift [p+1,end) left |
| Compact/filter | separate r,w | separate read,write |
| Remaining | n - i | end - p |
Array compaction
T *write = a;
for (T *read = a; read < end; read++) {
if (keep(*read))
*write++ = *read;
}
end = write;
Unordered remove while scanning
for (T *p = a; p < end;) {
if (remove(*p))
*p = *--end;
else
p++;
}
After replacement, stay on p: the element moved from the back has not been examined yet.
Singly linked lists
| Intent | Idiom |
|---|---|
| Traverse | for (Node *p = head; p; p = p->next) |
| Prepend | node->next = head; head = node; |
| Find | while (p && !match(p)) p = p->next; |
| Pointer-to-pointer cursor | Node **pp = &head; |
| Advance link cursor | pp = &(*pp)->next; |
| Remove current | *pp = (*pp)->next; |
| Insert before current | node->next = *pp; *pp = node; |
| Append location | while (*pp) pp = &(*pp)->next; |
List remove while scanning
Node **pp = &head;
while (*pp) {
if (remove(*pp))
*pp = (*pp)->next;
else
pp = &(*pp)->next;
}
pp points to the link that points to the current node.
List reverse
Node *prev = NULL;
Node *p = head;
while (p) {
Node *next = p->next;
p->next = prev;
prev = p;
p = next;
}
head = prev;
Array stack
/* invariant: 0 <= top && top <= cap */
stack[top++] = x; /* push */
x = stack[--top]; /* pop */
x = stack[top-1]; /* peek */
if (top == 0) { /* empty */ }
if (top == cap) { /* full */ }
Error handling
if (bad)
return ERR_BAD;
int err;
if ((err = step1()) != 0)
return err;
if ((err = step2()) != 0)
return err;
Prefer guard clauses over deep nesting.
Cleanup
int err = -1;
A *a = NULL;
B *b = NULL;
a = acquire_a();
if (!a) goto cleanup;
b = acquire_b();
if (!b) goto cleanup;
err = 0;
cleanup:
release_b(b);
release_a(a);
return err;
Acquire forward. Release backward. One cleanup path.
Ownership transfer
dst->ptr = src->ptr;
src->ptr = NULL;
Transfer the resource, then clear the old owner.
First-element initialization
if (n == 0)
return false;
T min = a[0];
for (size_t i = 1; i < n; i++) {
if (a[i] < min)
min = a[i];
}
Avoid arbitrary sentinels when the first element can seed the state.
Dynamic array
typedef struct {
T *data;
size_t len;
size_t cap;
} Vec;
/* invariant */
0 <= len && len <= cap
/* grow before write */
if (len == cap)
grow();
data[len++] = x;
Safe resize assignment
T *p = resize(v->data, new_cap);
if (!p)
return error;
v->data = p;
v->cap = new_cap;
Do not overwrite the only live pointer until resizing succeeds.
Hash map: SoA + indexed slots + NIL
/* slot encoding */
0 /* NIL / empty */
SIZE_MAX /* tombstone */
1..len /* live dense indices */
/* dense or parallel storage */
Key keys[data_cap];
Value values[data_cap];
/* index 0 is reserved */
keys[0] /* NIL key */
values[0] /* NIL value */
Using index 0 as NIL makes zero-initialized indices immediately valid and removes index translation: a live slot directly contains the dense index.
Hash lookup/probe
size_t k = hash(key) & (table_cap - 1);
for (;;) {
size_t i = slots[k];
if (i == 0)
return 0; /* NIL / not found */
if (i != SIZE_MAX &&
key_equal(&keys[i], key))
return i;
k = (k + 1) & (table_cap - 1);
}
Dense storage removal
keys[i] = keys[len];
values[i] = values[len];
len--;
/* then repair whichever table slot
referred to the moved element */
With index 0 reserved, live elements occupy 1..len. Unordered removal can still move the last live element into the removed slot.
ZII / NIL-index design
| 0 == NIL | Zero-initialized indices naturally mean “no object”. |
| {0} | Structures containing indices begin in a valid empty state without custom initialization. |
| items[0] | Reserve a real sentinel object so NIL reads can remain memory-safe. |
| return 0 | Lookup can return NIL instead of a separate error sentinel. |
| 1..len | Live objects use nonzero indices; zero remains globally meaningful. |
| SIZE_MAX | An out-of-band tombstone remains available for open addressing. |
typedef struct {
size_t parent;
size_t first_child;
size_t next_sibling;
} Node;
/* valid empty state */
Node node = {0};
A NIL object makes access at index 0 safe, but a raw write to items[0] is not automatically a semantic no-op: it mutates the sentinel. If NIL writes must be discarded, enforce that in the mutation operation.
High-value patterns to internalize
| p/end | Half-open pointer range: [p,end). |
| read/write | Filtering, compaction, decoding, transformation. |
| a[n++] / a[--n] | Count doubles as next-free position. |
| *p = *--end | O(1) unordered removal. |
| Node **pp | Manipulate the link to the current node; eliminates head special cases. |
| guard clauses | Reject errors early; keep the main path shallow. |
| goto cleanup | Centralize partial-resource cleanup. |
| temporary resize pointer | Preserve ownership across allocation failure. |
| 0 == NIL | Zero-is-initialization for index-based structures. |
| 0 / SIZE_MAX / 1..len | NIL/empty, tombstone, and direct live indices for hash slots. |