malloc is the entry point for dynamic memory in C. It gives you a block of bytes on the heap, returns a pointer to the first byte, and leaves initialization up to you. That last part matters more than beginners expect: malloc allocates memory, but it does not clear it, and it does not know what type of object you intend to store there.
If you are coming from stack variables or arrays, malloc can feel abstract at first. The shortest useful mental model is this: the stack is for short-lived, automatically managed storage, while the heap is for memory you request manually and release manually. That tradeoff is powerful, but it also means you are responsible for avoiding leaks, dangling pointers, and buffer overruns.
What malloc actually does
malloc stands for memory allocation. In C, you usually include it through #include <stdlib.h>. Its signature is roughly:
void *malloc(size_t size);
It accepts the number of bytes you want and returns a void * pointing to a block of memory large enough to hold that many bytes. If allocation fails, it returns NULL.
A few practical facts matter immediately:
- The returned pointer is untyped until you cast or assign it.
- The bytes are uninitialized.
- The block must later be released with
free. - The allocated region is contiguous.
That last point makes malloc especially useful for arrays, structs, and binary buffers.
A first example
Here is the simplest form of malloc usage for an integer:
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int));
if (p == NULL) {
printf("Allocation failed\n");
return 1;
}
*p = 42;
printf("%d\n", *p);
free(p);
return 0;
}
This example does three things correctly:
- It asks for enough bytes for one
int. - It checks whether the allocation worked.
- It frees the memory when done.
Notice that malloc(sizeof(int)) does not create an int in the same sense that int x; does. It creates raw storage that can hold an int once you put one there.
Why sizeof matters
You should almost never hardcode allocation sizes for typed objects. Use sizeof so the code stays correct across platforms and remains readable.
For a single struct:
MyType *item = malloc(sizeof(MyType));
For an array:
int *numbers = malloc(count * sizeof(int));
A common best practice is to write:
int *numbers = malloc(count * sizeof *numbers);
That version is safer because it derives the element size from the pointer variable itself. If the type changes later, the allocation expression usually changes with it.
When to use malloc
malloc is the right tool when:
- the size is not known at compile time
- the object must outlive the current function
- you need a large buffer that should not live on the stack
- you want to build dynamic data structures such as linked lists, trees, or resizable arrays
It is usually not the right tool when:
- the object is tiny and only needed inside one function
- a fixed-size local array is simpler
- you can use a higher-level container in another language or library
The rule is not “use malloc whenever possible.” The rule is “use it when manual lifetime control is actually needed.”
malloc versus calloc and realloc
These three functions are related, but they solve different problems.
| Function | What it does | Common use |
|---|---|---|
malloc | Allocates raw uninitialized memory | New objects, buffers, arrays |
calloc | Allocates and zero-initializes memory | Arrays that should start at 0 |
realloc | Resizes an existing allocation | Growing or shrinking dynamic buffers |
calloc is often helpful when you want known initial values. realloc is useful for dynamic growth, but you need to handle failure carefully so you do not lose the original pointer.
Example of calloc:
int *arr = calloc(10, sizeof(int));
This gives you 10 integers, all initialized to zero.
Common mistakes
The hardest part of malloc is not calling it. It is managing the memory safely afterward.
1. Forgetting to free
If you allocate memory and never release it, the program leaks memory.
char *buffer = malloc(1024);
if (buffer == NULL) return 1;
/* use buffer */
free(buffer);
Every successful allocation should have a clear ownership path that ends in free.
2. Using the pointer after free
Once memory is freed, the pointer becomes invalid. Accessing it is undefined behavior.
free(p);
*p = 10; /* wrong */
A good habit is to set it to NULL after freeing if the variable remains in scope.
3. Allocating the wrong size
This is subtle and dangerous.
int *arr = malloc(10); /* wrong: 10 bytes, not 10 ints */
If int is 4 bytes, you only allocated room for 2 or 3 integers, depending on alignment and platform assumptions. Always multiply by the element size.
4. Forgetting to check for NULL
Allocation can fail.
char *data = malloc(n);
if (data == NULL) {
/* handle failure */
}
Ignoring failure makes later code brittle and can crash the program.
5. Losing the original pointer on realloc
This is a classic bug:
p = realloc(p, new_size);
If realloc fails, it returns NULL and the original pointer is still valid, but you may have overwritten it and lost access to the block. Use a temporary variable instead.
void *tmp = realloc(p, new_size);
if (tmp == NULL) {
/* p is still valid here */
} else {
p = tmp;
}
A safer pattern for arrays
When working with dynamic arrays, it helps to follow a repeatable pattern:
size_t count = 100;
int *values = malloc(count * sizeof *values);
if (values == NULL) {
return 1;
}
for (size_t i = 0; i < count; i++) {
values[i] = (int)i;
}
free(values);
This pattern keeps the code readable and reduces mistakes:
- the count is a
size_t - the allocation size matches the element type
- the pointer is checked before use
- the memory is freed once it is no longer needed
malloc for structs
One of the most common uses of malloc is allocating a struct dynamically.
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
int age;
} Person;
Person *p = malloc(sizeof *p);
if (p == NULL) {
return 1;
}
strncpy(p->name, "Ava", sizeof p->name - 1);
p->name[sizeof p->name - 1] = '\0';
p->age = 28;
free(p);
This is useful when:
- the struct is large
- the struct should be shared across functions
- the object needs to survive after the current function returns
If the object does not need that lifetime, a local variable is usually simpler:
Person p;
Ownership discipline
The most important habit with malloc is to decide who owns the memory.
A few clean rules help:
- The function that allocates should document who frees.
- If a function receives a pointer, decide whether it borrows or owns it.
- Use
freeexactly once for each successful allocation. - Do not share the same pointer casually across unrelated parts of the code unless the ownership model is explicit.
This is why many C codebases establish conventions early. Without them, the code becomes hard to reason about.
Practical checklist
Before you use malloc, run through this checklist:
- Did you include
<stdlib.h>? - Are you allocating the right number of bytes?
- Did you check the return value against
NULL? - Have you initialized the memory before reading it?
- Do you know exactly where
freewill happen? - Are you avoiding use-after-free and double-free bugs?
If the answer to any of these is unclear, pause and fix the ownership model before adding more code.
Common comparison with stack allocation
Here is the practical difference:
- Stack allocation is automatic and fast.
- Heap allocation is manual and flexible.
Use stack allocation when the lifetime is local and the size is fixed. Use malloc when the lifetime or size is dynamic.
Example:
void f(void) {
int local = 5; /* stack */
int *heap = malloc(sizeof *heap); /* heap */
if (heap != NULL) {
*heap = local;
free(heap);
}
}
The stack variable disappears automatically when f returns. The heap allocation stays alive until free is called.
Bottom line
malloc is simple at the syntax level and demanding at the design level. If you remember only three things, make them these:
- allocate the correct number of bytes
- check for failure
- free what you allocate
Once those habits are automatic, malloc becomes a reliable tool for arrays, structs, buffers, and any C program that needs dynamic memory.
For a deeper next step, study calloc, realloc, and common ownership patterns in real C code. That is where dynamic memory moves from basic usage into maintainable programming practice.