JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Designated Initializers, Compound Literals, Flexible Arrays

Three quiet C99 features that, used together, let you build complex objects clearly, conjure temporaries inline, and pack a header and a payload into a single allocation. None of them are new syntax for show — each removes a real source of bugs.

Naming what you set: designated initializers

By now you have built plenty of structs the old way: list the values in a brace pair, in declaration order, and hope you remembered which slot is which. For a struct like `struct point { int x; int y; };` that means `struct point p = { 3, 4 };`, and you simply have to know that 3 lands in x and 4 in y. With two fields it is fine. With ten, and a couple of them the same type, positional initialization becomes a quiet trap: swap two values and the compiler says nothing, because both slots accept an int.

A designated initializer, from C99, lets you name the field you are setting with a leading dot: `struct point p = { .x = 3, .y = 4 };`. Now the assignment is spelled out at the call site, so a reader (and the compiler) can see exactly where each value goes — and you may write them in any order you like. The same idea works for arrays with `[index]`: `int days[7] = { [0] = 1, [6] = 1 };` sets the two weekend slots and leaves the rest alone. This is not cosmetic. Code that says what it means is code whose bugs you can see.

There is one behavior you should rely on deliberately: any field you do not mention is zero-initialized, exactly as if you had written `= {0}`. So `struct config c = { .port = 8080 };` leaves every other member set to zero, not to garbage. That single rule is what makes designated initializers robust to change. Add a new field to the struct next year and every existing `{ .port = ... }` site still compiles, with the new field a clean zero rather than an uninitialized landmine — and you have met uninitialized variables enough by now to respect how much that buys you.

Objects with no name: compound literals

Designated initializers describe what goes into an object. A compound literal lets you conjure the object itself, right where you need it, without first declaring a named variable for it. The syntax is a type in parentheses followed by a braced initializer: `(struct point){ .x = 3, .y = 4 }`. That expression is an object — a real one, with storage and an address — so you can pass it straight to a function: `draw((struct point){ .x = 3, .y = 4 });`. No temporary variable, no second line.

The detail that makes compound literals more than a shorthand is that the result is a true lvalue — it has an address you can take. `int *q = &(int){ 42 };` is a valid pointer to an int holding 42, materialized out of thin air. Inside the braces you may use the very same designated-initializer syntax from the last section, so the two features click together: `&(struct config){ .port = 8080 }` gives you a pointer to a fully-formed, mostly-zero config without ever naming it. It is a beautiful way to pass small fixed objects by address.

One header, a variable tail: flexible array members

Now to a feature that solves a concrete shape of problem: you want one block of memory holding a small header followed by a variable number of elements — a length, then the bytes; a packet header, then its payload. The naive answer is two allocations, a header struct and a separate buffer it points to, with two `free()` calls and an extra chance to leak or dangle — forget the second free() and you have a memory leak. A flexible array member, made official in C99, packs both into a single allocation instead.

You declare it as an array with no size, and it must be the last member of the struct: `struct buf { size_t len; char data[]; };`. That empty `data[]` contributes nothing to `sizeof(struct buf)` — the size is computed as if the array had length zero. The trick is in how you allocate: you ask malloc() for the header plus however many elements you want, in one call. `malloc(sizeof *b + n)` hands you a struct whose `data` array you may legally index from 0 to n-1, and a single free() releases the whole thing.

struct buf { size_t len; char data[]; };   // data[] adds 0 to sizeof

size_t n = 100;
struct buf *b = malloc(sizeof *b + n);      // header + 100 bytes, ONE alloc
if (b == NULL) { return -1; }               // always check malloc
b->len = n;
b->data[0] = 'H';                           // indices 0..n-1 are valid
/* ... use b->data ... */
free(b);                                     // one free releases both parts
sizeof ignores data[]; you over-allocate the tail yourself, index it from 0 to n-1, and one free() reclaims the whole block.

The honest caveats are worth saying plainly. The flexible member must be last, and the struct must have at least one other member. You cannot make an array of such structs, nor embed one inside another struct — the compiler would not know how big each one is. And you must over-allocate yourself: a plain local `struct buf b;` gives `data` exactly zero usable elements, so writing `b.data[0]` is an out-of-bounds access and undefined behavior. The feature gives you the packing; the sizing arithmetic and the malloc() check are still yours to get right.

A worked example, the three together

These features are friends, and a tiny message-builder shows all three pulling in the same direction. We want a function that allocates a message — a header plus a variable run of bytes — fills the header with named fields, and returns it. Walk through it and notice where each feature earns its place.

  1. Declare the type with a flexible array member last: `struct msg { int kind; size_t len; char body[]; };`. The `body[]` is the variable tail; `sizeof(struct msg)` counts only kind and len.
  2. Allocate header plus payload in one call, then check it: `struct msg *m = malloc(sizeof *m + n); if (m == NULL) return NULL;` — never skip checking malloc for NULL.
  3. Set the named fields directly: `m->kind = 7; m->len = n;`. (You cannot use a brace designated initializer on the already-allocated `*m`, but the named-field clarity carries over to the assignments.)
  4. Fill the tail through `m->body`, indices 0 to n-1, then hand the single block back. The caller frees it with one free(), header and payload released together.

Where does the compound literal fit? At the call site, when you pass small fixed inputs without ceremony. Suppose a helper takes a configuration: `send_with((struct opts){ .retries = 3, .verbose = true }, m);`. The opts object is built inline with named fields, lives exactly as long as the call, and never needs a name. Three features, one habit: say what you mean, build it where you use it, and let the allocation match the shape of the data — a struct whose tail grows to fit.

What you carry forward

Step back and the through-line is clear: each of these C99 features removes a specific class of mistake. Designated initializers kill the silently-swapped positional argument and the field you forgot to zero. Compound literals remove the throwaway named variable — while honestly handing you a lifetime rule you must respect, because their block-scoped lifetime is exactly where the dangling-pointer bug hides. Flexible array members collapse two allocations into one and make the header-plus-payload layout a first-class thing the language understands.

One last connective thread to the rest of this rung. When you over-allocate a flexible array member, the alignment and padding you met earlier still apply — the tail begins after any padding the header needs, and if the elements demand stronger alignment you may need alignas or aligned_alloc(). And all of this lives inside the same abstract machine the next guide refines: a compound literal's lifetime, a designated initializer's zeroing, a flexible array's bounds are all rules the standard states precisely, so honor them and the optimizer rewards you. The final guide in this rung — on variadic macros, C23 attributes, and a refined view of the abstract machine — picks up exactly there.