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; elsemalloc
+ explicit init (memset
or field-wise). - After
realloc
, init the new tail (bytes beyond old size). -
Never use raw
strdup
in our codebase. Usexstrdup
/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) failedn", 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) failedn", 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) failedn", 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] = ' ';
return z;
}
size_t n = strlen(s);
/* +1 checked overflow */
if (n >= SIZE_MAX) {
fprintf(stderr, "FATAL: xstrdup overflown");
abort();
}
char *p = xmalloc(n + 1);
memcpy(p, s, n + 1); /* includes ' ' */
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] = ' ';
return z;
}
size_t m = 0;
size_t m = strnlen(s, maxlen);
if (m >= SIZE_MAX) {
fprintf(stderr, "FATAL: xstrndup overflown");
abort();
}
char *p = xmalloc(m + 1);
if (m){
memcpy(p, s, m);
}
p[m] = ' ';
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 */
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);
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);
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 */
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);
}
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;
}
6) Anti-Patterns (Don’t Do These)
- Reading before init:
T *p = xmalloc(sizeof(*p));
if (p->flag) { /* UB */ }
- Assuming
realloc
clears new bytes. - Freeing borrowed pointers, or leaking owned ones.
- Using
alloca
for large/variable buffers. - Using unsafe string funcs (
strcpy
,strcat
, legacystrncpy
).
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 */ }
8) Checklist
- [ ] Use
xcalloc
for structs; orxmalloc
+memset
. - [ ] Always init after
malloc
/alloca
. - [ ] After
realloc
, init new bytes. - [ ] Use
xstrdup
/xstrndup
(never avoidstrdup
). - [ ] 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.