DEV Community

Cover image for The Guide to Safe & Modern C Memory Allocation Strategy
L Djohari
L Djohari

Posted on

The Guide to Safe & Modern C Memory Allocation Strategy

C gives you power and footguns to shoot yourself in the foot if you use it wrong.
This note standardizes how we allocate, initialize, copy, and free memory and strings in a portable, safe way (ISO C + small, documented helpers).
We avoid undefined behavior (UB), platform gotchas, and “hidden” memory allocation traps.

0) TL;DR Rules

  • Allocation ≠ Initialization. Never read uninitialized memory.
  • Prefer calloc for structs; else malloc + explicit init (memset or field-wise).
  • After realloc, init the new tail (bytes beyond old size).
  • Never use raw strdup in our codebase. Use xstrdup/xstrndup below.
  • Centralize lifetime with create/destroy APIs. Document ownership (borrowed vs owned).
  • After free, set pointer to NULL.

1) Core Allocation APIs

Function Purpose Initialized? Must Init?
malloc(n) Allocate n bytes (heap) ❌ garbage ✅ memset or assign
calloc(c,n) Allocate c*n bytes (heap) ✅ zeroed
realloc(p,n) Resize block p to n bytes 🔸 partial¹ ✅ init new tail
alloca(n) Allocate n bytes (stack, auto free) ❌ garbage ✅ memset or assign

¹ realloc: preserves old bytes; new bytes are uninitialized.

Golden rule: Never evaluate memory you didn’t initialize.


2) Safe Allocation Wrappers (ISO C, OOM-fatal, OOM-NULL with *_try variants)

Use these consistent behavior:

#ifndef SAFE_ALLOC_H
#define SAFE_ALLOC_H

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <limits.h>

/* Abort-on-OOM (Out of Memory) policy (common for daemons/servers).
   If you need non-fatal OOM, use *_try variants separately. */

static inline void *xmalloc(size_t size) {
    void *p = malloc(size);
    if (!p && size) {
        fprintf(stderr, "FATAL: malloc(%zu) failed\n", size);
        abort();
    }
    return p;
}

static inline void *xmalloc_try(size_t size) {
    void *p = malloc(size);
    if (!p && size) {
        return NULL;
    }
    return p;
}

static inline void *xcalloc(size_t nmemb, size_t size) {
    /* basic overflow guard */
    if (size && nmemb > SIZE_MAX / size) {
        fprintf(stderr, "FATAL: calloc overflow (%zu,%zu)\n", nmemb, size);
        abort();
    }
    void *p = calloc(nmemb, size);
    if (!p && nmemb && size) {
        fprintf(stderr, "FATAL: calloc(%zu,%zu) failed\n", nmemb, size);
        abort();
    }
    return p;
}

static inline void *xcalloc_try(size_t nmemb, size_t size) {
    /* basic overflow guard */
    if (size && nmemb > SIZE_MAX / size) {
        return NULL;
    }
    void *p = calloc(nmemb, size);
    if (!p && nmemb && size) {
        return NULL;
    }
    return p;
}


static inline void *xrealloc(void *ptr, size_t size) {
    void *p = realloc(ptr, size);
    if (!p && size != 0) {
        fprintf(stderr, "FATAL: realloc(%p,%zu) failed\n", ptr, size);
        abort();
    }
    return p;
}

static inline void *xrealloc_try(void *ptr, size_t size) {
    void *p = realloc(ptr, size);
    if (!p && size != 0) {
        return NULL;
    }
    return p;
}

/* Free + NULL 
*  We put scope in here to avoid dangling if-else on non braces statement
*/
#define xfree(p) { if ((p) != NULL) { free(p); p = NULL; } }

/* Bounded str duplicate: must provide valid cstr with NUL-terminate. */
static inline char *xstrdup(const char *s) {
    if (!s) {
        /* Define our policy: duplicate NULL → empty string */
        char *z = xmalloc(1);
        z[0] = '\0';
        return z;
    }
    size_t n = strlen(s);
    /* +1 checked overflow */
    if (n >= SIZE_MAX) {
        fprintf(stderr, "FATAL: xstrdup overflow\n");
        abort();
    }

    char *p = xmalloc(n + 1);
    memcpy(p, s, n + 1); /* includes '\0' */
    return p;
}

/* Bounded str duplicate: copy at most n bytes, n is size-1 (withouth NUL-terminate). */
static inline char *xstrndup(const char *s, size_t n) {
    if (!s) {
        char *z = xmalloc(1);
        z[0] = '\0';
        return z;
    }

    size_t m = 0;

    size_t m = strnlen(s, maxlen);
    if (m >= SIZE_MAX) {
        fprintf(stderr, "FATAL: xstrndup overflow\n");
        abort();
    }

    char *p = xmalloc(m + 1);

    if (m){
        memcpy(p, s, m);
    }

    p[m] = '\0';
    return p;
}

/* Binary memcopy helper. */
static inline void *xmemcpy(const void *src, size_t n) {
    if (!src && n) {
        return NULL;
    }

    void *p = xmalloc(n ? n : 1);
    if (n){ 
        memcpy(p, src, n);
    }

    return p;
}

#endif /* SAFE_ALLOC_H */
Enter fullscreen mode Exit fullscreen mode

Why not use strdup directly?

  • Our xstrdup checks for overflow and defines behavior for NULL input.
  • No surprises and consistent with our OOM policy.

3) Initialization Patterns

Structs (configs/contexts)

typedef struct {
    const char *host;
    int         port;
    int         backlog;
    void       *user_data;
} server_config_t;

/* Prefer calloc for zero/NULL defaults */
server_config_t *cfg = xcalloc(1, sizeof *cfg);
cfg->host   = xstrdup("0.0.0.0"); 
cfg->port   = 9000;
cfg->backlog= 256;

/* Or: malloc + memset + field init */
server_config_t *cfg2 = xmalloc(sizeof *cfg2);
memset(cfg2, 0, sizeof *cfg2);
cfg2->port = 9000;

xfree(cfg->host);
xfree(cfg);
xfree(cfg2);
Enter fullscreen mode Exit fullscreen mode

realloc tail must be initialized

size_t old_cap = 16, new_cap = 64;
char *buf = xmalloc(old_cap);
/* ...write up to old_cap... */

buf = xrealloc(buf, new_cap);
/* New region [old_cap, new_cap) is garbage → initialize if you will read it */
memset(buf + old_cap, 0, new_cap - old_cap);
Enter fullscreen mode Exit fullscreen mode

4) Ownership Conventions (VERY IMPORTANT)

Define clearly who allocates, who frees.

  • Borrowed pointer: points to memory we do not free (e.g., literals, caller-owned).
  • Owned pointer: allocated here, must be freed in destroy.
  • Copy-on-set: safest—API duplicates input and manages it.

Example API

typedef struct server server_t;

server_t *server_create(void);
void      server_destroy(server_t *s);

int        server_set_host(server_t *s, const char *host); /* copies */
const char*server_get_host(const server_t *s);             /* borrowed */
Enter fullscreen mode Exit fullscreen mode

Implementation:

struct server {
    char  *host;   /* owned */
    int    port;
    size_t backlog;
};

server_t *server_create(void) {
    server_t *s = xcalloc(1, sizeof(server_t));
    s->port = 9000;
    s->backlog = 256;
    return s;
}

int server_set_host(server_t *s, const char *host) {
    char *copy = xstrdup(host);  /* safe */
    xfree(s->host);
    s->host = copy;
    return 0;
}

const char *server_get_host(const server_t *s) {
    return s->host ? s->host : "";
}

void server_destroy(server_t *s) {
    if (!s) return;
    xfree(s->host);
    xfree(s);
}
Enter fullscreen mode Exit fullscreen mode

5) Create/Destroy + Heap Initialization

typedef struct {
    const char *host;
    int         port;
    int         backlog;
    size_t      read_chunk;
} uvsvr_config_t;

uvsvr_config_t uvsvr_config_defaults(void) {
    return (uvsvr_config_t){
        /* AS MOST C Library API always stated if char *p must be allocated, not literal*/
        .host        = xstrdup("0.0.0.0"), 
        .port        = 9000,
        .backlog     = 256,
        .read_chunk  = 16 * 1024,
    };
}

uvsvr_config_t *uvsvr_config_new(void) {
    uvsvr_config_t *p = xmalloc(sizeof *p);
    *p = uvsvr_config_defaults();
    return p;
}
Enter fullscreen mode Exit fullscreen mode

6) Anti-Patterns (Don’t Do These)

  • Reading before init:
  T *p = xmalloc(sizeof(*p));
  if (p->flag) { /* UB */ }
Enter fullscreen mode Exit fullscreen mode
  • Assuming realloc clears new bytes.
  • Freeing borrowed pointers, or leaking owned ones.
  • Using alloca for large/variable buffers.
  • Using unsafe string funcs (strcpy, strcat, legacy strncpy).

7) Practical Snippets

Safe formatting

char buf[64];
int n = snprintf(buf, sizeof(buf), "%s", input);
if (n < 0) { /* format error */ }
if ((size_t)n >= sizeof(buf)) { /* truncated */ }
Enter fullscreen mode Exit fullscreen mode

8) Checklist

  • [ ] Use xcalloc for structs; or xmalloc + memset.
  • [ ] Always init after malloc/alloca.
  • [ ] After realloc, init new bytes.
  • [ ] Use xstrdup/xstrndup (never avoid strdup).
  • [ ] Centralize lifetime in create/destroy.
  • [ ] Define ownership in headers.
  • [ ] xfree after use → pointer = NULL.
  • [ ] Avoid alloca in production servers.
  • [ ] Prefer snprintf/memcpy with explicit sizes.

Summary:

  • malloc/alloca = garbage → must init.
  • calloc = zeroed defaults.
  • realloc = old preserved, new garbage.
  • Use safe wrappers (xmalloc, xcalloc, xrealloc, xstrdup, etc.).
  • Centralize allocation in create/destroy APIs.
  • Document the ownership contracts.
  • This avoids UB, leaks, dangling pointers, and footguns/shooting yourself in the foot.

Top comments (1)

Collapse
 
pauljlucas profile image
Paul J. Lucas

The only memory allocation function that can take NULL is realloc. For all others, it's implementation defined. IMHO, you should do:

assert( size > 0 );
Enter fullscreen mode Exit fullscreen mode

Then you don't even need xmalloc_try since malloc already returns NULL upon failure.


You're putting way too much code in inline functions — they very likely will not actually be inlined.


calloc should be used only when you're initializing something like an array. calloc is inefficient for structs that you're going to initialize anyway since you're initializing the whole thing to zeros, then immediately overwriting the zeros. The better way to initialize structures is like:

server_config_t *cfg = malloc( sizeof *cfg );
*cfg = (server_config_t){
    .host = strdup( "0.0.0.0" ),
    .port = 9000,
    .backlog = 256
};
Enter fullscreen mode Exit fullscreen mode

Any members not explicitly initialized will automatically be initialized to zero for you.


For xfree, you never need to check for NULL first. By definition in the standard, free of a null pointer is guaranteed to do nothing. Also, the standard trick is to use do-while:

#define xfree(P)      do { free( PTR ); PTR = NULL; } while (0)
Enter fullscreen mode Exit fullscreen mode

But the problem will having a macro like that at all is that it doesn't work for pointers that aren't lvalues.