malloc & free
Memory Layout of a C Program
A running C program divides memory into four main regions:
- Text segment. The compiled machine code. Read-only.
- Data segment. Global and static variables with initial values, plus constants.
- Stack. Local variables and function call frames. Grows and shrinks automatically. Fixed size (typically 1-8 MB).
- Heap. Dynamically allocated memory. You control its lifetime. Can grow as needed (limited by system memory).
The heap is where malloc and friends operate.
malloc: Allocate Raw Bytes
malloc allocates a block of memory on the heap and returns a void* pointer to it. The memory is uninitialized.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *nums = malloc(5 * sizeof(int));
if (nums == NULL) {
fprintf(stderr, "malloc failed\n");
return 1;
}
for (int i = 0; i < 5; i++) {
nums[i] = (i + 1) * 10;
}
for (int i = 0; i < 5; i++) {
printf("nums[%d] = %d\n", i, nums[i]);
}
free(nums);
return 0;
}
nums[0] = 10
nums[1] = 20
nums[2] = 30
nums[3] = 40
nums[4] = 50
Always use sizeof with the variable, not the type, so the size stays correct if the type changes:
int *p = malloc(n * sizeof(*p)); /* preferred */
int *p = malloc(n * sizeof(int)); /* works but fragile */
calloc: Allocate & Zero-Initialize
calloc allocates memory for an array of elements and sets all bytes to zero:
int *nums = calloc(100, sizeof(int));
if (!nums) {
perror("calloc");
return 1;
}
/* All 100 ints are guaranteed to be 0 */
calloc takes two arguments: the number of elements and the size of each element. It also checks for integer overflow in the multiplication, which malloc(n * size) does not.
Use calloc when you need zero-initialized memory. Use malloc when you plan to fill every byte before reading.
realloc: Resize an Allocation
realloc changes the size of an existing allocation. It may move the memory to a new location.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int capacity = 4;
int count = 0;
int *arr = malloc(capacity * sizeof(*arr));
if (!arr) return 1;
for (int i = 0; i < 10; i++) {
if (count >= capacity) {
capacity *= 2;
int *tmp = realloc(arr, capacity * sizeof(*tmp));
if (!tmp) {
free(arr);
fprintf(stderr, "realloc failed\n");
return 1;
}
arr = tmp;
}
arr[count++] = i * i;
}
for (int i = 0; i < count; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
return 0;
}
0 1 4 9 16 25 36 49 64 81
Never assign realloc directly back to the original pointer:
/* WRONG: if realloc fails, arr is lost and the old memory leaks */
arr = realloc(arr, new_size);
/* CORRECT: use a temporary pointer */
int *tmp = realloc(arr, new_size);
if (!tmp) { /* handle error, arr is still valid */ }
arr = tmp;
When realloc succeeds, the old pointer is invalid (possibly freed). Only use the new pointer.
free: Release Memory
free returns memory to the heap. After free, the pointer is dangling and must not be used.
int *data = malloc(100 * sizeof(int));
/* ... use data ... */
free(data);
data = NULL; /* good practice: prevent accidental use */
Setting the pointer to NULL after freeing is a defensive habit. Dereferencing NULL crashes immediately with a clear error, while dereferencing a freed pointer produces unpredictable behavior.
free(NULL) is safe and does nothing.
Always Check if malloc Returned NULL
malloc, calloc, and realloc return NULL when they fail (out of memory). Using a NULL pointer is undefined behavior.
struct Node *node = malloc(sizeof(*node));
if (node == NULL) {
fprintf(stderr, "Out of memory\n");
exit(EXIT_FAILURE);
}
In small programs, exiting on allocation failure is reasonable. In libraries, return an error code so the caller decides what to do.
The Ownership Rule
Whoever allocates memory is responsible for freeing it. This must be documented clearly.
/* Caller owns the returned string and must free it */
char *format_greeting(const char *name) {
char *buf = malloc(256);
if (!buf) return NULL;
snprintf(buf, 256, "Hello, %s!", name);
return buf;
}
int main(void) {
char *msg = format_greeting("Alice");
if (msg) {
printf("%s\n", msg);
free(msg); /* caller's responsibility */
}
return 0;
}
Hello, Alice!
If ownership is ambiguous, memory leaks and double frees are inevitable.
Double Free: Undefined Behavior
Freeing the same pointer twice is undefined behavior. It can crash, corrupt the heap, or create exploitable security vulnerabilities.
int *p = malloc(sizeof(int));
free(p);
free(p); /* UNDEFINED BEHAVIOR */
Setting pointers to NULL after freeing prevents accidental double frees, since free(NULL) is safe:
free(p);
p = NULL;
free(p); /* safe: free(NULL) is a no-op */
Real-World Example: A Dynamic String
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct DynString {
char *data;
size_t length;
size_t capacity;
};
struct DynString *dynstr_create(void) {
struct DynString *s = malloc(sizeof(*s));
if (!s) return NULL;
s->capacity = 16;
s->data = malloc(s->capacity);
if (!s->data) { free(s); return NULL; }
s->data[0] = '\0';
s->length = 0;
return s;
}
int dynstr_append(struct DynString *s, const char *text) {
size_t text_len = strlen(text);
size_t needed = s->length + text_len + 1;
if (needed > s->capacity) {
size_t new_cap = s->capacity;
while (new_cap < needed) new_cap *= 2;
char *tmp = realloc(s->data, new_cap);
if (!tmp) return -1;
s->data = tmp;
s->capacity = new_cap;
}
memcpy(s->data + s->length, text, text_len + 1);
s->length += text_len;
return 0;
}
void dynstr_destroy(struct DynString *s) {
if (s) {
free(s->data);
free(s);
}
}
int main(void) {
struct DynString *s = dynstr_create();
if (!s) return 1;
dynstr_append(s, "Hello");
dynstr_append(s, ", ");
dynstr_append(s, "world!");
printf("String: \"%s\" (length=%zu, capacity=%zu)\n",
s->data, s->length, s->capacity);
dynstr_destroy(s);
return 0;
}
String: "Hello, world!" (length=13, capacity=16)
This demonstrates malloc for initial allocation, realloc for growth, and free for cleanup.
Real-World Example: Reading an Entire File
#include <stdio.h>
#include <stdlib.h>
char *read_file(const char *path, size_t *out_size) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size < 0) { fclose(f); return NULL; }
char *buf = malloc(size + 1);
if (!buf) { fclose(f); return NULL; }
size_t read = fread(buf, 1, size, f);
fclose(f);
buf[read] = '\0';
if (out_size) *out_size = read;
return buf; /* caller must free */
}
int main(void) {
size_t size;
char *content = read_file("example.txt", &size);
if (content) {
printf("Read %zu bytes\n", size);
free(content);
}
return 0;
}
Common Pitfalls
- Not checking for NULL.
malloccan fail. DereferencingNULLis undefined behavior. Always check. - Using memory after free. The pointer is dangling. Set it to
NULLafter freeing. - Memory leaks. Every
mallocneeds a matchingfree. If a function has multiple return paths, ensure all of them free allocated memory (or transfer ownership to the caller). - Integer overflow in size calculations.
malloc(n * sizeof(int))overflows silently ifnis very large. Usecallocfor overflow-checked allocation, or check the multiplication manually. - Mixing allocation functions. Memory allocated with
mallocmust be freed withfree. Do not mix with C++new/deleteor platform-specific allocators. - Forgetting to free realloc's old pointer on failure. If
reallocfails, the original pointer is still valid and must still be freed.
Key Takeaways
mallocallocates raw bytes.callocallocates and zeros.reallocresizes.freereleases.- Always check for
NULLreturn values from allocation functions. - Every allocation needs exactly one
free. Document ownership clearly. - Use
sizeof(*ptr)instead ofsizeof(Type)for resilient size calculations. - Double free is undefined behavior. Set pointers to
NULLafter freeing. - The heap is the only option for memory that must outlive the current scope or whose size is unknown at compile time.