Skip to main content

Dynamic Strings & Safe Concatenation

intermediate16 min readLesson 106 of 148

Building strings at runtime: the realloc-append pattern, and why every append is a (maybe-failing) allocation.

A growable string is a tiny dynamic array

typedef struct {
    char  *data;    /* malloc'd, always '\0'-terminated */
    size_t len;     /* characters, not counting '\0' */
    size_t cap;     /* allocated bytes */
} StrBuf;

Append is the whole API, and every append is three jobs in one: make room, copy, terminate โ€” with a failure path at step one:

int sb_append(StrBuf *b, const char *s) {
    size_t need = b->len + strlen(s) + 1;
    if (need > b->cap) {
        size_t ncap = b->cap ? b->cap : 16;
        while (ncap < need) ncap *= 2;
        char *nd = realloc(b->data, ncap);
        if (!nd) return -1;          /* old b->data still valid! */
        b->data = nd;
        b->cap = ncap;
    }
    memcpy(b->data + b->len, s, strlen(s) + 1);
    b->len += strlen(s);
    return 0;
}

Note the oom discipline: realloc returning NULL does not free the old block โ€” keep the pointer, return the error, let the caller decide. Assigning b->data = realloc(b->data, ...) directly would leak the old buffer exactly when memory is already exhausted.

Worst-case capacity planning

snprintf(NULL, 0, ...) measures first, so you can allocate exactly:

int need = snprintf(NULL, 0, "%s: %d", name, value);  /* +1 for '\0' */
char *out = malloc((size_t)need + 1);
snprintf(out, (size_t)need + 1, "%s: %d", name, value);

Measure-then-allocate is how code formats without fixed buffers and without truncation.

Now practice

Safe Copy GymBounded copies with honest truncation reporting, and a growable string buffer.2 challenges ยท ยท ~24 min