dev-c99-mini-idioms
Date: 2025-12-19
Zero-initialize scalars and aggregates with {0} (and know what it really does)
int x = 0;
int a[10] = {0};
struct S s = {0};
For objects with static storage duration, zero-init happens anyway; for locals, {0} forces a full zero fill by the rules for aggregate initialization. It is concise and hard to get wrong.
Designated initializers to document intent and avoid field-order coupling
struct Config cfg = {
.timeout_ms = 250,
.retries = 3,
.path = "/tmp/app.sock",
};
This makes changes to struct Config safer because you are not relying on declaration order.
Sparse array initialization with designated indices
int map[256] = { [0 ... 255] = -1, ['A'] = 0, ['B'] = 1 };
Range designators ([0 ... 255]) are a GCC/Clang extension, not C99. The strictly C99 part is designated indices like ['A']. If you need strict portability, stick to explicit indices.
Compound literals for "temporary objects with an address"
use_point(&(struct Point){ .x = 1, .y = 2 });
memcpy(dst, (unsigned char[]){0xDE,0xAD,0xBE,0xEF}, 4);
Compound literals are standard C99. They are extremely handy for passing small structs/buffers without naming a variable.
"Inline constant struct" as a default value without macros
static const struct Limits default_limits = { .min = 0, .max = 100 };
struct Limits lim = default_limits;
You get a single source of truth and avoid repeating the initializer.
Flexible "build up an initializer" with trailing commas
int xs[] = {
1,
2,
3,
};
C allows the trailing comma. It reduces diffs when adding and removing entries.
Local static for expensive one-time setup inside a function
const char *lookup(int k) {
static const char *tbl[] = { "zero", "one", "two" };
return (k >= 0 && k < (int)(sizeof tbl / sizeof tbl[0])) ? tbl[k] : "unknown";
}
This avoids repeated initialization and keeps the table close to the code that uses it.
Use sizeof (type){0} to get a type size without naming an object
size_t n = sizeof (struct Header){0};
This is occasionally useful in macros and generic helpers where you do not want an actual variable.
"Sentinel at end" arrays for tables of variable length
struct KV { const char *k; int v; };
static const struct KV kvs[] = {
{ "alpha", 1 },
{ "beta", 2 },
{ 0, 0 }, /* sentinel */
};
for (const struct KV *p = kvs; p->k; ++p) { /* ... */ }
This avoids separately storing a count and is very readable for static tables.
enum as compile-time integer constants
enum { BUF_SZ = 4096, MAX_RETRIES = 5 };
unsigned char buf[BUF_SZ];
Unlike macros, enum constants participate in the language and show up better in debuggers.
Counted push with post-increment (your pattern) plus the "guard" form
if (array_count < ARRAY_CAP) {
array[array_count++] = value;
}
The idiom is best when paired with an explicit bounds guard close by.
Iterate pointers over arrays (often cleaner than indices)
for (int *p = a, *e = a + n; p != e; ++p) {
*p += 1;
}
You avoid repeated indexing arithmetic and it composes well with subranges.
"Two-pointer" walk for in-place filtering/compaction
size_t w = 0;
for (size_t r = 0; r < n; ++r) {
if (keep(xs[r])) xs[w++] = xs[r];
}
n = w;
This is the simplest correct in-place filter pattern.
Reverse loop without unsigned underflow traps
for (size_t i = n; i-- > 0; ) {
/* uses i = n-1 down to 0 */
}
This avoids i >= 0 (which is always true for size_t) and avoids underflow bugs.
"Advance before use" scanning with assignment in the condition
int c;
while ((c = *p++) != 0) {
/* use c */
}
This compresses scanning code, and ensures the increment is not accidentally duplicated.
Parse with strtol and keep the end pointer in the loop header
for (char *p = s; *p; ) {
char *end;
long v = strtol(p, &end, 10);
if (end == p) break; /* no progress */
/* use v */
p = end;
}
This is a robust skeleton for numeric tokenization.
"Loop that guarantees at least one iteration" with do { } while (0) (not just for macros)
do {
if (!init()) break;
if (!step()) break;
finish();
} while (0);
It reads like structured early-exit without introducing goto yet.
Switch-driven state machine with intentional fallthrough
switch (state) {
case S_START:
/* ... */
state = S_BODY;
/* fall through */
case S_BODY:
/* ... */
break;
}
The convenience is real, but comment intentional fallthrough because compilers warn and humans misread.
The canonical "walk a C string" pointer loop
for (const unsigned char *p = (const unsigned char *)s; *p; ++p) {
/* ... */
}
Using unsigned char avoids undefined behavior with the ctype family when chars are negative.
ctype functions: cast to unsigned char, and do not pass pointers
if (isalpha((unsigned char)c)) { /* ... */ }
isalpha(0) is defined to be false in the C locale, so isalpha('\0') is fine. Passing NULL is not "special"; it is just 0 as an integer constant, but treating it as a pointer concept is misleading. The real pitfall is isalpha((signed char)0xE9) which can be negative and is undefined unless you cast to unsigned char (or pass EOF).
memchr as a fast "find delimiter in bytes"
const unsigned char *q = memchr(p, '\n', (size_t)(end - p));
if (q) { /* found */ }
Useful for scanning non-null-terminated buffers, unlike strchr.
"Length-limited string scan" without copying
size_t i = 0;
for (; i < n && buf[i] != '\0'; ++i) { /* ... */ }
This is the safe pattern for possibly unterminated data.
snprintf for bounded formatting and detecting truncation
int m = snprintf(dst, dst_sz, "%s/%s", a, b);
if (m < 0 || (size_t)m >= dst_sz) { /* truncated or error */ }
The return value is the size that would have been written, which makes it easy to size buffers.
"Append pointer" pattern for building strings efficiently
char out[256];
char *w = out;
char *e = out + sizeof out;
w += snprintf(w, (size_t)(e - w), "x=%d ", x);
w += snprintf(w, (size_t)(e - w), "y=%d", y);
This keeps the write position explicit and avoids repeated strlen.
Tokenize without strtok by using strcspn and manual slicing
for (char *p = s; *p; ) {
p += strspn(p, " \t");
size_t len = strcspn(p, " \t");
if (len == 0) break;
/* token is p..p+len */
p += len;
}
No global state, re-entrant, and it works with custom delimiters.
Single-exit cleanup with goto
int f(void) {
int rc = -1;
FILE *fp = 0;
void *buf = 0;
fp = fopen("x", "rb");
if (!fp) goto out;
buf = malloc(4096);
if (!buf) goto out;
rc = 0;
out:
free(buf);
if (fp) fclose(fp);
return rc;
}
This scales when you add resources; you do not multiply cleanup code.
"Return value capture" so you do not lose error information
int rc = do_work();
if (rc) return rc;
Simple, but the important part is: never overwrite rc before you have consumed it.
Preserve errno across cleanup that might clobber it
int saved = errno;
/* cleanup that might call functions touching errno */
errno = saved;
return -1;
This matters when callers use errno to diagnose the original failure.
fgets loop with newline trimming in one place
while (fgets(line, sizeof line, fp)) {
line[strcspn(line, "\n")] = '\0';
/* use line */
}
This avoids repeated ad hoc newline logic.
"Close on success, free on failure" with ownership transfer
char *buf = malloc(n);
if (!buf) return -1;
*out = buf; /* transfer ownership */
buf = 0; /* prevent accidental free */
return 0;
Setting the local to 0 after transfer prevents double-free in a shared cleanup block.
The do-while(0) macro wrapper so statements behave like statements
#define CHECK(x) do { if (!(x)) return -1; } while (0)
This prevents surprises with if (cond) CHECK(...); else ....
Compile-time array count (only works for actual arrays)
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
Use it only when you truly have an array, not a pointer.
"Static assert" in C99 via typedef trick (portable)
#define STATIC_ASSERT(expr) typedef char static_assertion[(expr) ? 1 : -1]
STATIC_ASSERT(sizeof(int) == 4);
C11 has _Static_assert, but this works in C99 and fails at compile time.
Stringize and concatenate for generating names and messages
#define STR_(x) #x
#define STR(x) STR_(x)
#define CAT_(a,b) a##b
#define CAT(a,b) CAT_(a,b)
int CAT(tmp_, __LINE__);
const char *msg = "line " STR(__LINE__);
These are building blocks for debugging macros and generated identifiers.
X-macro tables: one list, many derived artifacts
/* list */
#define COLORS(X) \
X(RED) \
X(GREEN) \
X(BLUE)
/* enum */
#define X(name) name,
enum Color { COLORS(X) };
#undef X
/* to-string */
#define X(name) case name: return #name;
const char *color_name(enum Color c) {
switch (c) { COLORS(X) default: return "?"; }
}
#undef X
This avoids keeping enum, strings, and other metadata in sync by hand.
Optional debug logging compiled out cleanly
#ifdef DEBUG
#define LOG(...) fprintf(stderr, __VA_ARGS__)
#else
#define LOG(...) ((void)0)
#endif
((void)0) keeps expression contexts well-typed.
Check for overflow before multiply/add (portable pattern)
if (b != 0 && a > SIZE_MAX / b) return -1; /* a*b would overflow */
if (a > SIZE_MAX - b) return -1; /* a+b would overflow */
This is the reliable, standard way (no compiler builtins).
Align up/down with power-of-two alignment (common, but easy to botch)
size_t align_up(size_t x, size_t a) { return (x + (a - 1)) & ~(a - 1); }
size_t align_dn(size_t x, size_t a) { return x & ~(a - 1); }
Only valid when a is a power of two.
Bitset membership with unsigned shifts (avoid UB on signed)
unsigned mask = 1u << bit;
if (flags & mask) { /* set */ }
Use unsigned types for bit operations; left shift on signed can be undefined.
Branchless "is power of two" (and exclude 0)
int is_pow2(size_t x) { return x && ((x & (x - 1)) == 0); }
Tiny and used often in allocators and alignment code.
Byte extraction with masks and shifts (portable endianness-agnostic logic)
unsigned byte0 = (x >> 0) & 0xFFu;
unsigned byte1 = (x >> 8) & 0xFFu;
This avoids aliasing and alignment pitfalls of pointer casts.
Allocate using the pointed-to type, not repeating the type name
T *p = malloc(sizeof *p);
p = realloc(p, new_count * sizeof *p);
This survives type changes and reduces mismatch errors.
realloc with a temporary to avoid losing the original pointer on failure
void *tmp = realloc(p, n);
if (!tmp) { /* p still valid */ return -1; }
p = tmp;
This is one of the highest-value C habits you can adopt.
Zero-initialize heap objects with calloc when it matches the semantics
T *p = calloc(count, sizeof *p);
It communicates "I want zeroed memory" and can be optimized by the runtime.
Memset a struct via its address, not by casting
memset(&s, 0, sizeof s);
Straightforward, but still the correct idiom when {0} is not possible.
Strict aliasing safe type-punning: use memcpy, not pointer casts
float f;
uint32_t u;
memcpy(&u, &f, sizeof u);
Pointer-cast punning can break under optimization; memcpy is the portable way.
"Flexible array member" for variable-sized structs (C99 feature)
struct Msg {
uint32_t len;
unsigned char data[]; /* flexible array */
};
struct Msg *m = malloc(sizeof *m + payload_len);
m->len = payload_len;
memcpy(m->data, payload, payload_len);
This keeps header and payload contiguous and cache-friendly.
Container-of pattern (common, but not standard-library provided)
#define OFFSETOF(type, member) ((size_t)&(((type *)0)->member))
#define CONTAINER_OF(ptr, type, member) ((type *)((char *)(ptr) - OFFSETOF(type, member)))
Useful for intrusive data structures. It relies on well-known idioms, but it is also sharp: use only when you fully control types and lifetimes.
Return error code, write outputs through pointers
int parse(const char *s, struct Result *out);
Callers can stack-allocate struct Result and you avoid heap ownership questions.
"Optional output" pointers with a simple guard
if (out_value) *out_value = v;
This gives callers flexibility without multiplying function variants.
Size-in, size-out for buffers (and report required size)
int encode(char *dst, size_t dst_sz, size_t *out_sz);
On overflow, set *out_sz to the needed size and return an error.
Use const to document and enforce read-only inputs
int hash(const void *data, size_t n, uint32_t *out);
This eliminates entire classes of accidental mutation bugs.
"User data" callbacks (poor man's closures)
typedef int (*visit_fn)(void *user, const struct Node *n);
int walk(const struct Node *root, visit_fn fn, void *user);
This is the idiomatic C way to carry context into callbacks.
sscanf is tempting, but strto* with end pointers composes better
char *end;
long v = strtol(p, &end, 10);
if (end == p) { /* not a number */ }
p = end;
This avoids format-string corner cases and gives you precise control.
Use scansets in sscanf when you really do want it (bounded)
char key[32];
if (sscanf(line, "%31[^=]=%*s", key) == 1) { /* key before '=' */ }
The width prevents buffer overflow; the scanset is a powerful but underused feature.
"Trim in place" with two indices/pointers
char *p = s;
while (*p == ' ' || *p == '\t') ++p;
char *e = p + strlen(p);
while (e > p && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\n')) --e;
*e = '\0';
if (p != s) memmove(s, p, (size_t)(e - p) + 1);
This is a reusable skeleton for whitespace trimming without allocating.
Print size_t and pointers correctly
printf("n=%zu\n", n);
printf("p=%p\n", (void *)p);
These format specifiers prevent subtle UB and portability bugs.
Use __FILE__ and __LINE__ for trace points
fprintf(stderr, "%s:%d: failed\n", __FILE__, __LINE__);
This is a low-tech, high-value breadcrumb.
Hex dump loop that is careful about signedness
const unsigned char *b = buf;
for (size_t i = 0; i < n; ++i) printf("%02X", (unsigned)b[i]);
Casting avoids char sign surprises.
The comma operator for tight loops (use sparingly)
for (i = 0, p = a; i < n; ++i, ++p) { /* ... */ }
It is idiomatic in C, but readability drops if you overpack logic into the header.
Assignment in condition to express "read then test"
while ((n = fread(buf, 1, sizeof buf, fp)) != 0) { /* ... */ }
This is one of the clearest uses of assignment-in-condition: it matches the control flow of I/O.
Boolean normalization with !!
int ok = !!ptr;
Forces a clean 0/1 value, useful when you need strict boolean-like integers.
"Swap via temporary" plus the macro version (evaluate once)
#define SWAP(a,b) do { __typeof__(a) _t = (a); (a) = (b); (b) = _t; } while (0)
The macro version above uses __typeof__, which is a GNU extension, not C99. In strict C99, you typically hand-write swaps or use a function specialized per type.
offsetof from <stddef.h> for layout-aware code
size_t off = offsetof(struct S, member);
This is standard and safer than home-grown offset macros.
volatile only for hardware/async boundaries, not for "thread safety"
volatile uint32_t *reg = (volatile uint32_t *)0x40000000u;
*reg = 1;
This is a pattern you see in embedded C; the key is knowing that volatile is not a concurrency primitive.
Use restrict (C99) when you can prove non-aliasing
void saxpy(float *restrict y, const float *restrict x, size_t n, float a);
It can unlock optimizations and documents a strong contract.
"One definition of a flag set" via an enum and bit masks
enum {
F_READ = 1u << 0,
F_WRITE = 1u << 1,
F_EXEC = 1u << 2,
};
This is a clean, debugger-friendly alternative to raw #define flags.