C memory management & pointers
How C programs use memory, for readers of C fundamentals: where variables live, what a
pointer is, pointer arithmetic, malloc and friends, ownership and lifetimes, struct layout, the classic
memory bugs with the tools that catch them, and the allocation patterns real C code is built on. C23
throughout, with notes where C17 and C11 differ. C++ automates most of this with RAII and smart pointers:
see C++ fundamentals.
Process memory layout
A running program sees a virtual address space: addresses that the CPU and kernel map, page by page (typically 4 KiB), onto physical memory. Only mapped pages can be touched; anything else faults with a segmentation fault. On 64-bit Linux the layout looks like this:
| Region | Holds | Lifetime | Notes |
|---|---|---|---|
.text | machine code | whole run | read-only and executable |
.rodata | string literals, const globals | whole run | read-only: writing faults |
.data | globals and static variables with an initializer | whole run | copied from the executable file |
.bss | globals and static variables without one | whole run | zero-filled at startup; takes no space in the file |
| heap | malloc, calloc, realloc blocks | until free | grows toward higher addresses |
| memory-mapped | shared libraries, large malloc blocks, mmap | until unmapped | glibc serves big requests (≈128 KiB and up by default) with mmap |
| stack | locals, parameters, return addresses | until the function returns | typically 8 MiB for the main thread on Linux and macOS, 1 MiB on Windows |
#include <stdio.h>
#include <stdlib.h>
const char *greeting = "hi"; // the pointer: .data
// the bytes "hi": .rodata
int limit = 10; // .data (initialized)
int counter; // .bss (zero-initialized)
int main(void) {
static int calls; // .bss too: not on the stack
int local = 5; // stack
int *heap = malloc(sizeof *heap); // heap
if (heap == nullptr) return 1;
printf("rodata %p\n", (void *)greeting);
printf("data %p\n", (void *)&limit);
printf("bss %p %p\n", (void *)&counter,
(void *)&calls);
printf("heap %p\n", (void *)heap);
printf("stack %p\n", (void *)&local);
free(heap);
}$ ./where
rodata 0x562ae0bf9004
data 0x562ae0bfb030
bss 0x562ae0bfb03c 0x562ae0bfb038
heap 0x562b178ab2a0
stack 0x7ffc52d17cf8
$ size ./where
text data bss dec hex filename
1978 640 16 2634 a4a ./whereThe C standard does not talk about segments; it defines storage durations:
| Storage duration | Objects | Lifetime | Initial value |
|---|---|---|---|
| static | globals, static locals, string literals | the whole run | zero unless initialized |
| automatic | locals and parameters | until their block ends | indeterminate unless initialized |
| allocated | malloc family blocks | until free | indeterminate (calloc: zero) |
| thread | thread_local variables | until their thread ends | zero unless initialized |
What a pointer is
A pointer is a variable that holds a memory address, plus a type that says what lives at that address.
&x (address-of) gives a pointer to x; *p (dereference) is the object p points to, usable for reading
and writing.
#include <stdio.h>
int main(void) {
int x = 5;
int *p = &x; // p holds the address of x
*p = 7; // write through p: x is now 7
int **pp = &p; // a pointer to a pointer
printf("%d %d %d\n", x, *p, **pp); // 7 7 7
printf("%p %p\n", (void *)&x, (void *)p); // equal
printf("%zu\n", sizeof p); // 8 on 64-bit systems
}1/4
int x = 5; reserves 4 bytes on the stack at some address, here 0x7ffc…a4c, and stores 5 there.
| Expression | Meaning |
|---|---|
&x | address of x; its type is "pointer to the type of x" |
*p | the object at the address in p |
p->m | member m of the struct p points to: (*p).m |
p[i] | the i-th object after the one p points to: *(p + i) |
p == q | same address? (does not compare the pointed-to values) |
if (p), !p | non-null? null? |
sizeof p vs sizeof *p | size of the pointer (8) vs size of what it points to |
Read declarations from the name outward: right first, then left, with parentheses grouping.
| Declaration | x is |
|---|---|
int *x | a pointer to int |
int *x, y; | x a pointer, but y a plain int: the * binds to the name |
int **x | a pointer to a pointer to int |
int *x[10] | an array of 10 pointers to int |
int (*x)[10] | a pointer to an array of 10 int |
int *x(void) | a function returning a pointer to int |
int (*x)(int) | a pointer to a function taking int and returning int |
const char *x | a pointer to const char |
char *const x | a const pointer to char |
A null pointer points to no object. Dereferencing it is undefined behavior; in practice the null page is unmapped and the program crashes.
NULL | nullptr (C23) | |
|---|---|---|
| What it is | a macro: ((void *)0) or plain 0, depending on the library | a keyword of type nullptr_t |
| Converts to | any pointer; if it is 0, also any integer | any pointer and bool, nothing else |
As a variadic argument (execl(…, NULL)) | wrong if it is an int 0 and pointers are wider | always passed as a null pointer |
Static pointers start out null; local pointers start out with garbage (a wild pointer), so initialize
them, to nullptr if there is nothing better.
Pointer arithmetic and arrays
Adding an integer to a pointer moves it by that many elements, not bytes: p + 1 is sizeof *p bytes
further on. That is why the pointer's type matters.
#include <stddef.h>
#include <stdio.h>
int main(void) {
int a[5] = {10, 20, 30, 40, 50};
int *p = a; // decays: same as &a[0]
printf("%d %d %d\n", p[2], *(p + 2), a[2]); // 30 x 3
printf("%p %p\n", (void *)p, (void *)(p + 1));
// the two addresses differ by sizeof(int) == 4
ptrdiff_t d = &a[4] - &a[1]; // 3 elements
printf("%td\n", d);
for (int *it = a; it != a + 5; it++) // a + 5 is one
printf("%d ", *it); // past the end
printf("\n%zu %zu\n", sizeof a, sizeof p); // 20 8
}An array is not a pointer, but in almost every expression its name decays into a pointer to its first element:
| Expression | Type and value |
|---|---|
a (most uses) | int *, pointing at a[0] |
sizeof a | 20: the whole array (no decay) |
&a | int (*)[5]: the same address, but &a + 1 jumps 20 bytes |
sizeof p | 8: just the pointer |
a parameter int a[] or int a[5] | really int *a; sizeof a inside the function is 8, and GCC and Clang warn (-Wsizeof-array-argument) |
a = p; | does not compile: arrays cannot be assigned |
"abc" | char[4], decaying to char * |
#include <stddef.h>
#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
// The parameter is really `const int *xs`: pass the length
long sum(const int xs[], size_t n) {
long total = 0;
for (size_t i = 0; i < n; i++) total += xs[i];
return total;
}
long sum_all(void) {
static const int xs[] = {1, 2, 3, 4};
return sum(xs, ARRAY_LEN(xs)); // ARRAY_LEN on the real
} // array, never on xs[]Rules for pointer arithmetic:
- It is defined only inside one array and up to one past its end. Computing
a + 6ora - 1is undefined even if you never dereference it;a + 5may be computed and compared but not dereferenced. - Subtracting pointers gives a
ptrdiff_tcount of elements; both must point into the same array. ==and!=work on any two pointers;<,>only within one array.- Arithmetic on
void *is a GNU extension (-pedanticwarns): cast tounsigned char *for byte offsets. - A single object counts as an array of one, so
&x + 1is a valid one-past-the-end pointer.
char s[] = "hi"; | const char *s = "hi"; | |
|---|---|---|
| What it is | a 3-byte array holding a copy of the literal | a pointer to the literal itself |
| Writable | yes: s[0] = 'H'; | no: writing is undefined (use const so the compiler stops you) |
sizeof s | 3 | 8 |
s = other; | does not compile | fine: the pointer moves |
const and pointers
const applies to whatever is immediately to its left (or to its right when it comes first). With pointers
there are two things that can be read-only: the pointed-to object and the pointer itself.
| Declaration | Change *p? | Change p? | Read as |
|---|---|---|---|
const int *p (same as int const *p) | no | yes | pointer to const int |
int *const p | yes | no | const pointer to int |
const int *const p | no | no | const pointer to const int |
#include <stdio.h>
int main(void) {
int x = 1, y = 2;
const int *a = &x; // pointer to const int
// *a = 5; // error: *a is read-only
a = &y; // fine: a itself can move
int *const b = &x; // const pointer to int
*b = 5; // fine
// b = &y; // error: b is read-only
const int *const c = &x; // neither can change
printf("%d %d %d\n", *a, *b, *c); // 2 5 5
}- Put
conston pointer parameters a function only reads through:size_t strlen(const char *s). It documents the contract and lets callers pass read-only data. int *converts toconst int *implicitly (adding a promise is safe). Going back needs a cast, and writing through it to an object that was definedconstis undefined behavior.- The conversion is only automatic at the first level:
char **does not convert toconst char **, nor (in C, unlike C++) toconst char *const *. Declare the right type, or cast. constis shallow: aconst structwith achar *namemember still lets you modifyname[0].
Pointers to pointers and 2-D arrays
A pointer to a pointer lets a function change the caller's pointer: return a new allocation through an out-parameter, or unlink a node in a list (see the linked list recipe).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// On success sets *out to a new string (caller frees) and
// returns 0; on failure leaves *out alone, returns -1.
int join_path(const char *dir, const char *file,
char **out) {
size_t n = strlen(dir) + 1 + strlen(file) + 1;
char *s = malloc(n);
if (!s) return -1;
snprintf(s, n, "%s/%s", dir, file);
*out = s;
return 0;
}
int main(void) {
char *path = nullptr;
if (join_path("/etc", "hosts", &path) != 0) return 1;
puts(path); // /etc/hosts
free(path);
}Four ways to store a grid of rows × cols numbers:
| Form | Memory | Indexing | Freeing |
|---|---|---|---|
double m[3][4] | one contiguous block, row after row (row-major) | m[r][c] | automatic (stack) or static |
double (*m)[cols] = malloc(rows * sizeof *m) | one heap block; m points to rows of cols | m[r][c] | one free(m) |
double *m = malloc(rows * cols * sizeof *m) | one heap block | m[r * cols + c] | one free(m) |
double **m: an array of row pointers | rows + 1 separate blocks; rows may differ in length (jagged) | m[r][c] | free each row, then m |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
size_t rows = 3, cols = 4;
// Contiguous: one allocation; the type knows cols
double (*m)[cols] = malloc(rows * sizeof *m);
if (!m) return 1;
for (size_t r = 0; r < rows; r++)
for (size_t c = 0; c < cols; c++)
m[r][c] = (double)(r * cols + c);
printf("%.0f\n", m[2][3]); // 11
free(m);
// Jagged: an array of pointers, each row its own block
const char *words[] = {"ab", "cde", "f"};
char **rowsv = calloc(3, sizeof *rowsv);
if (!rowsv) return 1;
for (size_t i = 0; i < 3; i++)
rowsv[i] = strdup(words[i]); // null on failure
printf("%s\n", rowsv[1] ? rowsv[1] : "?"); // cde
for (size_t i = 0; i < 3; i++) free(rowsv[i]);
free(rowsv);
}double (*m)[cols] is a pointer to a variable-length array type. GCC and Clang support it, and C23 makes
such types mandatory again (while VLAs on the stack stay optional); MSVC supports neither, so portable code
uses the flat m[r * cols + c] form (matrix recipe). A double[3][4]
is not a double **: passing one where the other is expected is a type error, because there are no row
pointers in a contiguous block.
Function pointers and callbacks
A function's name decays to a pointer to it, and calling through the pointer looks like a normal call. This is how C does callbacks, plugins, strategy objects and dispatch tables.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { const char *name; int age; } Person;
// qsort calls this with pointers to two elements
static int by_age(const void *a, const void *b) {
const Person *x = a, *y = b;
return (x->age > y->age) - (x->age < y->age);
}
static int by_name(const void *a, const void *b) {
const Person *x = a, *y = b;
return strcmp(x->name, y->name);
}
int main(void) {
Person people[] = {{"Lin", 41}, {"Ada", 36}, {"Bo", 29}};
size_t n = sizeof people / sizeof people[0];
int (*cmp)(const void *, const void *) = by_age;
qsort(people, n, sizeof people[0], cmp);
printf("%s\n", people[0].name); // Bo
qsort(people, n, sizeof people[0], by_name);
printf("%s\n", people[0].name); // Ada
}Write the comparison as (x > y) - (x < y), not x - y, which overflows for large values.
Callbacks take a void * context argument so the caller can pass its own state through code that knows
nothing about it:
#include <stddef.h>
#include <stdio.h>
typedef void Visit(int value, void *ctx); // function type
static void for_each(const int *xs, size_t n, Visit *f,
void *ctx) {
for (size_t i = 0; i < n; i++) f(xs[i], ctx);
}
static void add_to(int v, void *ctx) {
*(long *)ctx += v;
}
typedef double BinOp(double, double);
static double add(double a, double b) { return a + b; }
static double mul(double a, double b) { return a * b; }
static const struct { char sym; BinOp *fn; } ops[] = {
{'+', add},
{'*', mul},
};
int main(void) {
int xs[] = {1, 2, 3, 4};
long total = 0;
for_each(xs, 4, add_to, &total);
printf("%ld\n", total); // 10
for (size_t i = 0; i < 2; i++) // dispatch table
printf("6 %c 7 = %g\n", ops[i].sym, ops[i].fn(6, 7));
}| Syntax | Meaning |
|---|---|
int (*f)(int) | f is a pointer to a function taking and returning int |
typedef int Fn(int); Fn *f; | the same, easier to read; typedef the function type |
f(3), (*f)(3) | both call it |
f = name; or f = &name; | both store the address |
| calling through the wrong type | undefined behavior: the signatures must match exactly |
function pointer ↔ void * | not allowed in ISO C (POSIX dlsym requires it to work anyway) |
void pointers and casting
void * is the generic object pointer: any object pointer converts to it and back without a cast and
without losing information. It is how malloc, memcpy, qsort and callbacks stay type-agnostic. You
cannot dereference it or do arithmetic on it; convert it to a typed pointer first.
#include <stdio.h>
#include <string.h>
// Works for any type: the caller supplies the size
void swap_any(void *a, void *b, size_t size) {
unsigned char *x = a, *y = b; // byte view
for (size_t i = 0; i < size; i++) {
unsigned char t = x[i];
x[i] = y[i];
y[i] = t;
}
}
int main(void) {
double p = 1.5, q = 2.5;
swap_any(&p, &q, sizeof p);
char s[8] = "left", t[8] = "right";
swap_any(s, t, sizeof s);
printf("%g %g %s %s\n", p, q, s, t);
} // 2.5 1.5 right left| Conversion | Rule |
|---|---|
T * → void * → T * | implicit both ways in C; gives back the original pointer |
malloc result | do not cast it in C: int *p = malloc(n * sizeof *p); (C++ requires the cast) |
any object → unsigned char * | always allowed: inspect or copy the bytes of anything |
T * → unrelated U *, then dereference | usually undefined behavior (strict aliasing, alignment): use memcpy (below) |
| pointer ↔ integer | uintptr_t u = (uintptr_t)p; for hashing or alignment math; implementation-defined |
casting away const | compiles; writing to a truly const object is UB |
Dynamic allocation
| Function | Does | On failure | Notes |
|---|---|---|---|
malloc(size) | allocates size bytes, uninitialized | returns null | malloc(0) returns null or a unique pointer; free either |
calloc(n, size) | allocates n * size bytes, zeroed | returns null | checks n * size for overflow (required since C23) |
realloc(p, size) | resizes: may grow in place or move, copying the old bytes; new bytes uninitialized | returns null, old block untouched | realloc(nullptr, n) is malloc(n); realloc(p, 0) is UB in C23 |
free(p) | releases a block from the functions above | free(nullptr) does nothing; p now dangles | |
aligned_alloc(align, size) | like malloc with a stronger alignment (C11) | returns null | free with free; keep size a multiple of align |
free_sized(p, size) | free with a size hint (C23) | glibc 2.43+ |
#include <stdio.h>
#include <stdlib.h>
typedef struct { double x, y; } Point;
int main(void) {
size_t n = 1000;
Point *pts = malloc(n * sizeof *pts); // type named once
if (pts == nullptr) {
perror("malloc");
return 1;
}
for (size_t i = 0; i < n; i++)
pts[i] = (Point){.x = (double)i, .y = 0.0};
int *counts = calloc(256, sizeof *counts); // all zero
if (!counts) { free(pts); return 1; }
// Grow: keep the old pointer until realloc succeeds
size_t bigger = n * 2;
Point *tmp = realloc(pts, bigger * sizeof *pts);
if (!tmp) { free(counts); free(pts); return 1; }
pts = tmp; // the block may have moved
n = bigger;
printf("%.0f %d %zu\n", pts[999].x, counts[0], n);
free(counts);
free(pts);
pts = nullptr; // a later use fails loudly
}| Idiom | Why |
|---|---|
p = malloc(n * sizeof *p); | the element type is written once; changing p's type cannot leave a stale sizeof(OldType) |
if (!p) … after every allocation | null on failure; dereferencing it is UB. On Linux, overcommit means malloc rarely fails, but huge or limited requests still do |
tmp = realloc(p, …); if (tmp) p = tmp; | p = realloc(p, …) loses (leaks) the old block when it returns null |
check n > SIZE_MAX / size before malloc(n * size) | the product can wrap around and allocate far too little |
free(p); p = nullptr; | turns a later use-after-free into a predictable null dereference, and a double free into a no-op |
no cast on malloc | void * converts implicitly in C; a cast only adds noise |
#include <stdint.h>
#include <stdlib.h>
// malloc(n * size) without the silent wrap-around
void *alloc_array(size_t n, size_t size) {
if (size != 0 && n > SIZE_MAX / size) return nullptr;
return malloc(n * size);
}malloc returns memory aligned for any standard type (alignof(max_align_t), 16 bytes on x86-64 Linux).
Each block costs some hidden bookkeeping and the allocator rounds sizes up, so a million tiny
allocations cost far more than one big one; see arenas and pools.
When a growable array runs out of room, realloc may have to move it:
1/5
malloc(2 * sizeof *xs) returns an 8-byte block at 0x5555…2a0. Its contents are indeterminate (?), so len = 0 and cap = 2.
Ownership and lifetimes
C has no garbage collector and no destructors. Every allocation needs an owner: the one piece of code responsible for freeing it exactly once. Nothing enforces this, so make it visible in names, comments and function pairs.
| Pattern | Example | Who frees |
|---|---|---|
| caller provides the buffer | snprintf(buf, n, …), fgets(buf, n, f) | the caller, often nothing to free (stack) |
| function returns a new allocation | strdup, make_id below | the caller, with free or a matching thing_free |
| create/destroy pair | fopen/fclose, counter_new/counter_free | the caller, through the destroy function |
| borrowed pointer | a const char *name parameter | nobody; the callee must not keep it after returning |
| pointer into static storage | strerror, getenv, asctime | nobody; overwritten by the next call, not thread-safe |
| ownership transferred in | "list_add takes ownership of item" | the receiver, from then on |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 1. Caller provides the buffer: no allocation at all
bool format_id(char *buf, size_t cap, int id) {
int n = snprintf(buf, cap, "user-%04d", id);
return n >= 0 && (size_t)n < cap;
}
// 2. Callee allocates; the comment says who frees.
// Returns a new string, or null. Caller must free() it.
char *make_id(int id) {
char tmp[32];
if (!format_id(tmp, sizeof tmp, id)) return nullptr;
return strdup(tmp); // C23 (POSIX before)
}
// 3. Static buffer: nothing to free, but the next call
// overwrites it and threads would share it
const char *id_str(int id) {
static char buf[32];
return format_id(buf, sizeof buf, id) ? buf : "?";
}
int main(void) {
char a[16];
if (format_id(a, sizeof a, 7)) puts(a); // user-0007
char *b = make_id(8);
if (b) { puts(b); free(b); } // user-0008
puts(id_str(9)); // user-0009
}What you must never do is return the address of a local:
char *f(void) { char buf[32] = "hi"; return buf; } compiles (with a warning) and hands the caller a pointer
into a stack frame that no longer exists.
1/7
main starts. Its frame holds a return address (where to continue when main ends) and its locals p and y, still indeterminate. rsp, the stack pointer register, marks the top of the stack, which grows toward lower addresses.
| Object | Its lifetime ends | A pointer to it afterward |
|---|---|---|
| local variable or parameter | at the } of its block | dangles |
| compound literal inside a function | at the end of the enclosing block | dangles |
| string literal | never | valid, but read-only |
static local | never | valid; shared by every call |
malloc'd block | at free, or at a realloc that moves it | dangles |
| an element of a dynamic array | when the array is reallocated | treat it as dangling, even if the block happened not to move this time |
| Use the stack when | Use the heap when |
|---|---|
| the size is fixed and small (up to a few KiB) | the size is only known at runtime, or large |
| the object is only needed during this call | it must outlive the function that creates it |
| speed matters: allocation is free | it is shared between objects, threads or modules |
| (many same-sized objects: a pool) | (many objects with one lifetime: an arena) |
Strings and buffers safely
A C string needs strlen(s) + 1 bytes: the '\0' is not counted by strlen but must be stored. sizeof
gives an array's capacity, but only for an array in scope; on a char * it gives 8.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
char buf[32] = "hello";
const char *p = buf;
printf("%zu %zu %zu\n", strlen(buf), sizeof buf,
sizeof p); // 5 32 8
size_t n = strlen(buf) + 1; // + 1 for '\0'
char *copy = malloc(n);
if (!copy) return 1;
memcpy(copy, buf, n);
// Bounded append: snprintf never writes past the cap
size_t len = strlen(buf);
snprintf(buf + len, sizeof buf - len, ", %s", "world");
printf("%s / %s\n", buf, copy); // hello, world / hello
free(copy);
}strncpy looks like a safe strcpy but is not: strncpy(d, "hello", 4) writes h e l l and no
terminator, and when the source is short it pads the rest of d with zeros. It was designed for
fixed-width record fields. A copy that always terminates and reports truncation:
#include <string.h>
// Copy src into dst[cap] (cap > 0), always terminated.
// Returns false if src was too long and got truncated.
bool str_copy(char *dst, size_t cap, const char *src) {
size_t n = strlen(src);
if (n >= cap) {
memcpy(dst, src, cap - 1);
dst[cap - 1] = '\0';
return false;
}
memcpy(dst, src, n + 1);
return true;
}| Instead of | Use | Why |
|---|---|---|
gets(buf) | fgets(buf, sizeof buf, stdin) | gets cannot be used safely; removed in C11 |
strcpy(d, s) | str_copy above, snprintf(d, cap, "%s", s), or check strlen then memcpy | no size limit |
strcat(d, s) | track the length; snprintf(d + len, cap - len, …) | no size limit, O(n) to find the end |
strncpy(d, s, n) | str_copy, or strlcpy where available (BSDs, macOS, glibc 2.38+, POSIX.1-2024) | may leave d unterminated |
sprintf(buf, …) | snprintf(buf, sizeof buf, …) | no size limit |
scanf("%s", w) | scanf("%63s", w) or fgets | no size limit |
atoi(s) | strtol (recipe) | no error reporting; UB on overflow |
strtok(s, ",") | strtok_r (POSIX), or scan with strcspn | hidden global state |
memset(key, 0, n) before free(key) | memset_explicit (C23; glibc 2.43+) or explicit_bzero | the optimizer may delete a memset of memory that is about to die |
Bytes are not characters: strlen counts bytes, so a UTF-8 string with accents is longer than it looks,
and cutting at an arbitrary byte can split a character. For binary data that may contain zero bytes, carry
a pointer and a length instead of relying on '\0'.
Structs in memory
Each member must sit at an address that is a multiple of its alignment (alignof): 1 for char, 4 for
int, 8 for double and pointers on 64-bit platforms. To get there, the compiler inserts padding bytes
between members and at the end.
#include <stddef.h> // offsetof
#include <stdio.h>
struct item { // offset size
char tag; // 0 1 then 7 bytes padding
double price; // 8 8
int qty; // 16 4 then 4 bytes padding
}; // sizeof 24, alignof 8
struct item_sorted { // the same fields, largest first
double price; // 0 8
int qty; // 8 4
char tag; // 12 1 then 3 bytes padding
}; // sizeof 16
int main(void) {
printf("%zu %zu %zu %zu\n", sizeof(struct item),
offsetof(struct item, qty), alignof(struct item),
sizeof(struct item_sorted)); // 24 16 8 16
}| Rule | Detail |
|---|---|
| member offset | a multiple of the member's alignment |
| struct alignment | the largest alignment of any member |
sizeof | rounded up to a multiple of the struct's alignment (tail padding), so every element of an array is aligned |
| order | members stay in declaration order; the compiler never reorders them, but you can |
| padding contents | unspecified: do not memcmp structs or fwrite them to files and sockets |
offsetof(T, m) | byte offset of m in T (<stddef.h>) |
-Wpadded | warns wherever padding is inserted; pahole (from the dwarves tools) prints the holes of compiled structs |
| packing | [[gnu::packed]] (GCC/Clang) or #pragma pack(1) removes padding; misaligned members are slower and taking their address is unsafe on some CPUs |
For file formats and network protocols, serialize field by field with an explicit byte order rather than writing a struct's bytes: padding, alignment and endianness differ between platforms.
Common memory bugs
| Bug | Tiny example | What happens | Caught by |
|---|---|---|---|
| leak | p = malloc(64); p = malloc(64); | the first block can never be freed; memory use grows | LeakSanitizer (part of ASan on Linux), Valgrind |
| double free | free(p); free(p); | allocator corruption; glibc often aborts with free(): double free detected | ASan, Valgrind, -fanalyzer |
| use after free | free(n); n = n->next; | reads or overwrites memory now used by something else | ASan, Valgrind |
| dangling pointer to a local | int *f(void) { int x = 1; return &x; } | garbage values or a crash much later | compiler warning (on by default), ASan (stack-use-after-return) |
| heap buffer overflow | char *s = malloc(5); strcpy(s, "hello"); | overwrites the allocator's bookkeeping or a neighbor block | ASan, Valgrind, -D_FORTIFY_SOURCE=2 with -O2 |
| stack buffer overflow | char b[8]; b[8] = 0; | overwrites other locals or the return address | ASan, -fstack-protector-strong; Valgrind misses most |
| off-by-one | for (i = 0; i <= n; i++) a[i] = 0;, malloc(strlen(s)) | one element, or the terminator, past the end | ASan |
| uninitialized read | int *a = malloc(n * sizeof *a); s += a[0]; | random values that change between runs and builds | Valgrind (--track-origins=yes), MemorySanitizer; -Wall for locals |
| invalid free | free(&x), free(p + 1), free("literal") | the allocator aborts or corrupts its state | ASan, Valgrind, -Wfree-nonheap-object |
| realloc leak | p = realloc(p, n); when it fails | the old block is lost | code review: realloc into a temporary |
| null dereference | using malloc's result unchecked | segmentation fault | -fanalyzer, UBSan |
| stack overflow | runaway recursion, or char big[16 << 20]; as a local | segmentation fault | ASan reports stack-overflow; ulimit -s shows the limit |
All of these are undefined behavior: the program may appear to work, especially in a debug build, and fail later or elsewhere. Run tests under a sanitizer instead of trusting a clean run.
1/5
malloc(16) returns a 16-byte block, here at 0x5555…2a0. strcpy fills 4 bytes; the other 12 are indeterminate.
Debugging tools
# AddressSanitizer + UBSan: rebuild with checks compiled in
cc -std=c23 -g -O1 -fno-omit-frame-pointer \
-fsanitize=address,undefined app.c -o app
./app
# Valgrind memcheck: no rebuild needed, much slower
cc -std=c23 -g -O0 app.c -o app
valgrind --leak-check=full --track-origins=yes ./app
# GCC's static analyzer: finds bugs without running
gcc -std=c23 -fanalyzer -c app.c
# Clang's static analyzer over a whole build
scan-build make| Tool | Finds | Cost | Notes |
|---|---|---|---|
AddressSanitizer (-fsanitize=address) | heap, stack and global overflows, use after free, double and invalid free, use after return | about 2× slower, several times the memory | GCC and Clang; MSVC has /fsanitize=address |
| LeakSanitizer | blocks still allocated at exit | almost none | runs automatically with ASan on Linux; standalone with -fsanitize=leak |
UndefinedBehaviorSanitizer (-fsanitize=undefined) | signed overflow, bad shifts, misaligned or null access, out-of-bounds indexes into arrays of known size | small | combine with ASan |
MemorySanitizer (-fsanitize=memory) | reads of uninitialized memory | about 3× slower | Clang only; every library the program uses must be instrumented too |
| Valgrind memcheck | heap errors, leaks, uninitialized reads | about 20–30× slower | no recompile; Linux and some other Unix systems; weak on stack arrays |
-fanalyzer | double free, leaks, null dereference, use after free along specific paths | compile time | GCC 10+; may report false positives |
-Wall -Wextra | returning a local's address, sizeof on an array parameter, uninitialized locals | free | always on |
-D_FORTIFY_SOURCE=2 (or 3) with -O1 or more | overflows into buffers of known size in memcpy, strcpy, sprintf | tiny | glibc; some distributions' GCC enable it by default |
ASan and Valgrind cannot be combined. Sample reports, from GCC 13 on x86-64 Linux, trimmed:
$ ./uaf # free(name); return name[0];
==5727==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010 ...
READ of size 1 at 0x502000000010 thread T0
#0 0x559481caa28d in main uaf.c:9
0x502000000010 is located 0 bytes inside of 8-byte region [0x502000000010,0x502000000018)
freed by thread T0 here:
#0 0x7fc0beefc4d8 in free ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:52
#1 0x559481caa24f in main uaf.c:8
previously allocated by thread T0 here:
#0 0x7fc0beefd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x559481caa23f in main uaf.c:5
SUMMARY: AddressSanitizer: heap-use-after-free uaf.c:9 in main$ gcc -fanalyzer -c df.c
df.c: In function 'process':
df.c:8:5: warning: double-'free' of 'buf' [CWE-415] [-Wanalyzer-double-free]
8 | free(buf);
| ^~~~~~~~~
'process': events 1-5
| 4 | int *buf = malloc(n * sizeof *buf);
| | (1) allocated here
| 5 | if (n > 10) {
| | (2) following 'true' branch (when 'n > 10')...
| 6 | free(buf);
| | (3) ...to here
| | (4) first 'free' here
| 8 | free(buf);
| | (5) second 'free' here; first 'free' was at (4)Read an ASan report top down: the kind of error and the faulting line, then where the memory was freed, then where it was allocated. Leak reports appear when the program exits; an example is in the leak recipe.
Patterns
goto cleanup
One exit path that frees everything acquired so far. Initialize every resource to null first; free(nullptr)
is a no-op, so the cleanup block needs no flags.
#include <stdio.h>
#include <stdlib.h>
// Sum the bytes of a file; returns 0 on success, -1 on error
int checksum(const char *path, unsigned long *out) {
int rc = -1;
FILE *f = nullptr;
unsigned char *buf = nullptr;
unsigned long sum = 0;
size_t n;
if (!(f = fopen(path, "rb"))) goto done;
if (!(buf = malloc(64 * 1024))) goto done;
while ((n = fread(buf, 1, 64 * 1024, f)) > 0)
for (size_t i = 0; i < n; i++) sum += buf[i];
if (ferror(f)) goto done;
*out = sum;
rc = 0;
done:
free(buf); // reverse order of acquisition
if (f) fclose(f);
return rc;
}Arena (bump) allocator
An arena hands out memory by bumping an offset through one big block and frees everything at once by
resetting the offset. Allocation is a few instructions, there is no per-object free to forget, and
everything with the same lifetime (one request, one frame, one parse) goes in the same arena.
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
unsigned char *base;
size_t cap, used;
} Arena;
bool arena_init(Arena *a, size_t cap) {
a->base = malloc(cap);
a->cap = a->base ? cap : 0;
a->used = 0;
return a->base != nullptr;
}
// align: a power of two, at most alignof(max_align_t)
void *arena_alloc(Arena *a, size_t size, size_t align) {
size_t start = (a->used + align - 1) & ~(align - 1);
if (start > a->cap || size > a->cap - start)
return nullptr; // out of space
a->used = start + size;
return a->base + start;
}
// n * sizeof(T) can overflow: check n if it is untrusted
#define ARENA_NEW(a, T, n) \
((T *)arena_alloc((a), (n) * sizeof(T), alignof(T)))
void arena_reset(Arena *a) { a->used = 0; } // free all
void arena_free(Arena *a) {
free(a->base);
*a = (Arena){};
}
int main(void) {
Arena a;
if (!arena_init(&a, 1 << 20)) return 1; // 1 MiB
for (int frame = 0; frame < 3; frame++) {
double *xs = ARENA_NEW(&a, double, 1000);
char *name = ARENA_NEW(&a, char, 32);
if (!xs || !name) break;
snprintf(name, 32, "frame %d", frame);
xs[999] = frame;
printf("%s: %zu bytes used\n", name, a.used);
arena_reset(&a); // the whole frame, freed
}
arena_free(&a);
}1/6
arena_init(&a, 1 << 20) makes one malloc of 1 MiB (glibc serves a block this big with mmap). base points to it, cap is 1048576 and used is 0.
Variations: chain a new block when one fills up instead of failing; save used and restore it later to
free just the most recent allocations (a scratch arena); give each thread its own arena.
Pools and free lists
A pool serves many objects of one size from a preallocated array. Free slots are linked together through their own memory (a free list), so allocating and freeing are O(1) pointer swaps with no fragmentation. Typical uses: particles, network connections, tree nodes. A complete version is in the pool recipe.
Opaque pointers
Declare a struct in the header without its fields. Callers can hold and pass Counter * but cannot see
inside or allocate one themselves, so the implementation can change without recompiling them: C's version of
private members.
#ifndef COUNTER_H
#define COUNTER_H
typedef struct Counter Counter; // incomplete type
Counter *counter_new(const char *name); // null on failure
void counter_add(Counter *c, long n);
long counter_total(const Counter *c);
void counter_free(Counter *c); // accepts null
#endif#include <stdlib.h>
#include <string.h>
#include "counter.h"
struct Counter { // only this file sees the fields
char *name;
long total;
};
Counter *counter_new(const char *name) {
Counter *c = malloc(sizeof *c);
if (!c) return nullptr;
c->name = strdup(name);
if (!c->name) { free(c); return nullptr; }
c->total = 0;
return c;
}
void counter_add(Counter *c, long n) { c->total += n; }
long counter_total(const Counter *c) { return c->total; }
void counter_free(Counter *c) {
if (!c) return;
free(c->name); // free what it owns, then itself
free(c);
}FILE from <stdio.h> works the same way: you only ever hold a FILE *.
Flexible array members
A struct's last member may be an array with no size. Allocate the header and the data in one block: one
malloc, one free, and the data sits right next to its length.
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
size_t len;
unsigned char bytes[]; // flexible array member (C99)
} Blob;
// Returns a new Blob holding a copy of src; free() it.
Blob *blob_new(const void *src, size_t len) {
if (len > SIZE_MAX - sizeof(Blob)) return nullptr;
Blob *b = malloc(sizeof *b + len); // header + data
if (!b) return nullptr;
b->len = len;
memcpy(b->bytes, src, len);
return b;
}sizeof(Blob) does not include bytes. A struct with a flexible array member cannot be an array element
or a member of another struct, and copying it by value copies only the header.
Advanced qualifiers and aliasing
restrict
restrict on a pointer parameter promises that, while the function runs, the object it points to is
accessed only through that pointer. The compiler can then keep values in registers and vectorize without
re-reading memory after each write. Breaking the promise is undefined behavior.
#include <stddef.h>
// dst and src must not overlap
void axpy(size_t n, float a, float *restrict dst,
const float *restrict src) {
for (size_t i = 0; i < n; i++) dst[i] += a * src[i];
}memcpy's parameters are restrict, memmove's are not:
memcpy(dst, src, n) | memmove(dst, src, n) | |
|---|---|---|
| Overlapping ranges | undefined behavior | handled correctly |
| Speed | fastest | about the same in practice |
| Use for | copying between distinct buffers | shifting data within one buffer: memmove(a, a + 1, (n - 1) * sizeof *a) |
volatile
volatile tells the compiler that every read and write of the object must really happen, in order, because
something outside the program's visible flow can change or observe it: memory-mapped hardware registers, a
signal handler, setjmp/longjmp. It does not make anything thread-safe; for threads use _Atomic
(<stdatomic.h>) or a mutex.
#include <signal.h>
#include <stdio.h>
static volatile sig_atomic_t stop = 0;
static void on_sigint(int) { stop = 1; } // C23: unnamed
int main(void) {
signal(SIGINT, on_sigint);
unsigned long long ticks = 0;
while (!stop) ticks++; // Ctrl-C ends the loop
printf("\nstopped after %llu ticks\n", ticks);
}Without volatile the compiler may read stop once, see 0 and turn the loop into while (1).
Strict aliasing and type punning
The strict aliasing rule: an object may only be accessed through an lvalue of its own type (or a
compatible, signed/unsigned or qualified version of it), or through a character type. Reading a float
through a uint32_t * is undefined, and optimizers at -O2 rely on the rule. To reinterpret bytes, copy
them:
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
uint32_t float_bits(float f) {
uint32_t u;
static_assert(sizeof u == sizeof f);
memcpy(&u, &f, sizeof u); // defined; compiles to a
return u; // single register move
// return *(uint32_t *)&f; // UB: strict aliasing
}
int main(void) {
printf("%08" PRIX32 "\n", float_bits(1.0f)); // 3F800000
}| Technique | Status in C |
|---|---|
memcpy into a variable of the other type | always defined; the compiler optimizes the call away |
reading another member of a union (union { float f; uint32_t u; }) | defined in C (it reinterprets the bytes); undefined in C++ |
unsigned char * view of any object | always allowed |
| casting a pointer and dereferencing it | undefined unless the types are compatible |
-fno-strict-aliasing | turns the optimization off (the Linux kernel builds with it) |
alignas, alignof and aligned_alloc
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
alignas(64) long hits; // its own 64-byte cache line
} PerThreadCounter;
int main(void) {
printf("%zu %zu\n", alignof(max_align_t),
sizeof(PerThreadCounter)); // 16 64
// 64-byte aligned, for SIMD or to avoid false sharing
void *p = aligned_alloc(64, 64 * 16); // size: k * 64
if (!p) return 1;
printf("%d\n", (uintptr_t)p % 64 == 0); // 1
free(p);
}alignas, alignof and static_assert are keywords in C23; C11 spells them _Alignas, _Alignof and
_Static_assert (or includes <stdalign.h> and <assert.h>). Alignment must be a power of two. A
pointer converted to a type with stricter alignment than its address has, then dereferenced, is undefined
behavior (UBSan's alignment check catches it).
Recipes
A safe growable vector
The workhorse dynamic array: amortized O(1) push by doubling, overflow-checked, never leaking on failure.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t len, cap;
} IntVec;
// Appends x. Returns false (v unchanged) if out of memory.
bool vec_push(IntVec *v, int x) {
if (v->len == v->cap) {
size_t cap = v->cap ? v->cap * 2 : 8;
if (cap > SIZE_MAX / sizeof *v->data) return false;
int *p = realloc(v->data, cap * sizeof *v->data);
if (!p) return false;
v->data = p;
v->cap = cap;
}
v->data[v->len++] = x;
return true;
}
// Bounds-checked read: false if i is out of range.
bool vec_get(const IntVec *v, size_t i, int *out) {
if (i >= v->len) return false;
*out = v->data[i];
return true;
}
void vec_free(IntVec *v) {
free(v->data);
*v = (IntVec){}; // data = null, len = cap = 0
}
int main(void) {
IntVec v = {};
for (int i = 0; i < 100; i++)
if (!vec_push(&v, i * i)) { vec_free(&v); return 1; }
int x;
if (vec_get(&v, 99, &x)) printf("%d\n", x); // 9801
printf("len %zu cap %zu\n", v.len, v.cap); // 100 128
vec_free(&v);
}Pointers to elements (&v.data[3]) become invalid on the next push that reallocates: store indexes instead.
A singly linked list
Insert, delete and free with a pointer to the link being changed (Node **), which removes the special case
for the head.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
// Inserts at *link; returns false if out of memory.
bool list_push(Node **link, int value) {
Node *n = malloc(sizeof *n);
if (!n) return false;
*n = (Node){.value = value, .next = *link};
*link = n;
return true;
}
// Inserts keeping the list in ascending order.
bool list_insert_sorted(Node **head, int value) {
Node **link = head;
while (*link && (*link)->value < value)
link = &(*link)->next;
return list_push(link, value);
}
// Removes the first node holding value; true if found.
bool list_remove(Node **head, int value) {
for (Node **link = head; *link; link = &(*link)->next) {
if ((*link)->value == value) {
Node *dead = *link;
*link = dead->next; // unlink, then free
free(dead);
return true;
}
}
return false;
}
void list_free(Node **head) {
Node *n = *head;
while (n) {
Node *next = n->next; // read before free
free(n);
n = next;
}
*head = nullptr;
}
int main(void) {
Node *head = nullptr;
int xs[] = {5, 1, 4, 2, 3};
for (size_t i = 0; i < 5; i++)
if (!list_insert_sorted(&head, xs[i])) break;
list_remove(&head, 4);
for (Node *n = head; n; n = n->next)
printf("%d ", n->value); // 1 2 3 5
putchar('\n');
list_free(&head);
}1/5
After inserting 5, 1, 4 and 2 the list is 1 → 2 → 4 → 5. Each node is its own malloc(16) block, 32 bytes apart because malloc adds a header and rounds up, and in allocation order (5 came first), not list order. Inserting 3 starts with link = &head.
Read a whole file into memory
For configs, small data files and pipes: one heap buffer grown by doubling. Works for stdin too, unlike
tricks based on fseek and ftell.
#include <stdio.h>
#include <stdlib.h>
// Reads all of f into a new buffer with a '\0' after the
// data. Returns null on error. Caller frees the result.
char *read_all(FILE *f, size_t *len_out) {
size_t cap = 4096, len = 0;
char *buf = malloc(cap);
if (!buf) return nullptr;
for (;;) {
len += fread(buf + len, 1, cap - len - 1, f);
if (len < cap - 1) break; // end of file or error
char *tmp = realloc(buf, cap * 2);
if (!tmp) { free(buf); return nullptr; }
buf = tmp;
cap *= 2;
}
if (ferror(f)) { free(buf); return nullptr; }
buf[len] = '\0'; // handy for text; data may hold 0s
if (len_out) *len_out = len;
return buf;
}
int main(int argc, char *argv[]) {
FILE *f = argc > 1 ? fopen(argv[1], "rb") : stdin;
if (!f) { perror(argv[1]); return 1; }
size_t len = 0;
char *data = read_all(f, &len);
if (f != stdin) fclose(f);
if (!data) { perror("read"); return 1; }
printf("%zu bytes\n", len);
free(data);
}A 2-D matrix in one allocation
For numeric grids of runtime size: one block, row-major, freed with a single free, and portable to
compilers without VLA types.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
size_t rows, cols;
double data[]; // rows * cols values, row after row
} Matrix;
// Zero-filled rows x cols matrix, or null. free() it.
Matrix *mat_new(size_t rows, size_t cols) {
size_t room = SIZE_MAX - sizeof(Matrix);
room /= sizeof(double); // most doubles that fit
if (cols != 0 && rows > room / cols) return nullptr;
size_t n = rows * cols;
Matrix *m = calloc(1, sizeof *m + n * sizeof m->data[0]);
if (!m) return nullptr;
m->rows = rows;
m->cols = cols;
return m;
}
static inline double *mat_at(Matrix *m, size_t r,
size_t c) {
return &m->data[r * m->cols + c];
}
int main(void) {
Matrix *a = mat_new(2, 3);
if (!a) return 1;
for (size_t r = 0; r < a->rows; r++)
for (size_t c = 0; c < a->cols; c++)
*mat_at(a, r, c) = (double)(r * 10 + c);
printf("%g\n", *mat_at(a, 1, 2)); // 12
free(a); // one free
}Loop with the column index innermost: consecutive c values are adjacent in memory, which is far faster
than striding down columns.
A fixed-size pool with a free list
For many short-lived objects of one type: O(1) allocate and release, no per-object malloc, and the whole
pool freed at once.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct { float x, y, vx, vy; } Particle;
typedef union Slot { // a free slot holds the link,
union Slot *next; // a used one holds the object
Particle item;
} Slot;
typedef struct {
Slot *slots;
Slot *free_list;
} Pool;
bool pool_init(Pool *p, size_t n) {
*p = (Pool){};
if (n == 0 || n > SIZE_MAX / sizeof(Slot)) return false;
p->slots = malloc(n * sizeof *p->slots);
if (!p->slots) return false;
for (size_t i = 0; i + 1 < n; i++) // chain the slots
p->slots[i].next = &p->slots[i + 1];
p->slots[n - 1].next = nullptr;
p->free_list = p->slots;
return true;
}
Particle *pool_alloc(Pool *p) { // O(1)
Slot *s = p->free_list;
if (!s) return nullptr; // pool exhausted
p->free_list = s->next;
return &s->item;
}
void pool_release(Pool *p, Particle *item) { // O(1)
Slot *s = (Slot *)item; // a union and its members
s->next = p->free_list; // share one address
p->free_list = s;
}
void pool_destroy(Pool *p) {
free(p->slots); // every object at once
*p = (Pool){};
}
int main(void) {
Pool pool;
if (!pool_init(&pool, 1024)) return 1;
Particle *a = pool_alloc(&pool);
Particle *b = pool_alloc(&pool);
if (!a || !b) return 1;
*a = (Particle){.x = 1, .vx = 0.5f};
pool_release(&pool, a);
Particle *c = pool_alloc(&pool); // reuses a's slot
printf("%d\n", c == a); // 1
pool_destroy(&pool);
}Split a string into owned tokens
When a function returns several allocations: a null-terminated array of strings plus one function that frees
all of them, the same shape as argv.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void free_tokens(char **toks) {
if (!toks) return;
for (char **t = toks; *t; t++) free(*t);
free(toks);
}
// Splits s at every sep. Returns a null-terminated array of
// new strings (free with free_tokens), or null on failure.
char **split(const char *s, char sep) {
size_t n = 1;
for (const char *c = s; *c; c++) n += (*c == sep);
char **toks = calloc(n + 1, sizeof *toks); // + null
if (!toks) return nullptr;
const char delim[2] = {sep, '\0'};
for (size_t i = 0; i < n; i++) {
size_t len = strcspn(s, delim);
toks[i] = strndup(s, len); // C23
if (!toks[i]) { free_tokens(toks); return nullptr; }
s += len + 1;
}
return toks;
}
int main(void) {
char **parts = split("red,green,,blue", ',');
if (!parts) return 1;
for (size_t i = 0; parts[i]; i++)
printf("[%s] ", parts[i]); // [red] [green] [] [blue]
putchar('\n');
free_tokens(parts);
}Find a leak with ASan and Valgrind
When memory use grows or a test suite needs a leak check: build with ASan (LeakSanitizer reports at exit), or run an unmodified binary under Valgrind.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Returns a heap copy of s with a '!' appended.
// The caller owns the result and must free() it.
static char *shout(const char *s) {
size_t n = strlen(s);
char *out = malloc(n + 2);
if (out == nullptr) return nullptr;
memcpy(out, s, n);
out[n] = '!';
out[n + 1] = '\0';
return out;
}
int main(void) {
const char *words[] = {"hi", "hey", "yo"};
for (size_t i = 0; i < 3; i++) {
char *loud = shout(words[i]);
if (loud) puts(loud);
// BUG: missing free(loud);
}
}gcc -std=c23 -g -fsanitize=address leak.c -o leak && ./leak
gcc -std=c23 -g -O0 leak.c -o leak && \
valgrind --leak-check=full ./leak==5812==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 13 byte(s) in 3 object(s) allocated from:
#0 0x7fb721efd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x55bfbb0852f8 in shout leak.c:9
#2 0x55bfbb085530 in main leak.c:20
SUMMARY: AddressSanitizer: 13 byte(s) leaked in 3 allocation(s).
==5820== HEAP SUMMARY:
==5820== in use at exit: 13 bytes in 3 blocks
==5820== total heap usage: 4 allocs, 1 frees, 4,109 bytes allocated
==5820==
==5820== 13 bytes in 3 blocks are definitely lost in loss record 1 of 1
==5820== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==5820== by 0x1091F8: shout (leak.c:9)
==5820== by 0x10929E: main (leak.c:20)Both name the allocation site (shout, line 9) and its caller (main, line 20): the fix is a free(loud);
after puts. "Definitely lost" means no pointer to the block remains; "still reachable" blocks are pointed to
by globals at exit and are usually harmless. The extra Valgrind allocation is stdout's 4 KiB buffer.
References
- cppreference: Memory management library (opens in a new tab):
malloc,calloc,realloc,free,aligned_allocand the C23 additions - cppreference: Pointer declaration (opens in a new tab): pointer syntax, null pointers and pointer arithmetic
- cppreference: Objects and alignment (opens in a new tab): storage, effective types and the strict aliasing rule
- N3220 working draft (opens in a new tab): the freely available draft closest to C23 (memory management is clause 7.24.3)
- Clang: AddressSanitizer (opens in a new tab), LeakSanitizer (opens in a new tab) and MemorySanitizer (opens in a new tab): what each detects and its runtime options
- GCC: Program instrumentation options (opens in a new tab):
-fsanitize=and-fstack-protectorin GCC - GCC: Static analyzer options (opens in a new tab):
-fanalyzerand its warnings - Valgrind: Memcheck manual (opens in a new tab): reading memcheck's error messages and leak kinds
- SEI CERT C Coding Standard (opens in a new tab): its Memory Management (MEM) rules cover allocation, freeing and lifetimes, with compliant and non-compliant code
- Beej's Guide to C Programming (opens in a new tab): friendly chapters on pointers, arrays and manual memory management