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
callocfor structs; elsemalloc+ explicit init (memsetor field-wise). - After
realloc, init the new tail (bytes beyond old size). -
Never use raw
strdupin our codebase. Usexstrdup/xstrndupbelow. - 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 */
Why not use strdup directly?
- Our
xstrdupchecks 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
reallocclears new bytes. - Freeing borrowed pointers, or leaking owned ones.
- Using
allocafor 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
xcallocfor 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.
- [ ]
xfreeafter use → pointer = NULL. - [ ] Avoid
allocain production servers. - [ ] Prefer
snprintf/memcpywith 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)
The only memory allocation function that can take
NULLisrealloc. For all others, it's implementation defined. IMHO, you should do:Then you don't even need
xmalloc_trysincemallocalready returnsNULLupon failure.You're putting way too much code in
inlinefunctions — they very likely will not actually be inlined.callocshould be used only when you're initializing something like an array.callocis inefficient forstructs 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:Any members not explicitly initialized will automatically be initialized to zero for you.
For
xfree, you never need to check forNULLfirst. By definition in the standard,freeof a null pointer is guaranteed to do nothing. Also, the standard trick is to usedo-while:But the problem will having a macro like that at all is that it doesn't work for pointers that aren't lvalues.