malloc is a function in C that lets you request a block of memory while your program is running, rather than having the compiler decide memory sizes before execution. It takes a number of bytes as its argument and returns a pointer to the start of a freshly allocated block on the heap. If the allocation fails, it returns a null pointer instead. Understanding malloc is essential for any C program that needs to work with data whose size isn’t known at compile time.
How malloc Differs From Regular Variables
When you declare a normal variable inside a function, it lives on the stack. Stack memory is managed automatically: space is created when the function is called and destroyed when it returns. This is fast and convenient, but it comes with limitations. You can’t return a locally created array from a function and expect it to survive, and you can’t decide at runtime how large that array should be (in standard, portable C).
Heap memory, where malloc allocates, works differently. It persists until you explicitly free it. That means you can allocate memory in one function, pass a pointer to it elsewhere, and the data stays valid for as long as you need it. The tradeoff is that you’re now responsible for cleaning it up.
The Function Signature
To use malloc, you include <stdlib.h>. The function signature looks like this:
void *malloc(size_t size);
It takes a single argument: the number of bytes you want. It returns a void *, which is a generic pointer that can be assigned to any pointer type. In C, there’s an implicit conversion between void * and other pointer types, so you don’t need to cast the result. Writing int *p = malloc(sizeof(int) * 10); is perfectly valid and is considered the cleaner style in C. Casting is only required in C++, which has stricter type rules.
Using malloc With sizeof
Since malloc works in raw bytes, you almost always pair it with the sizeof operator to request the right amount of space. To allocate an array of 1,000 integers:
int *arr = malloc(sizeof(int) * 1000);
This works for any type, including structs. If you have a struct pixel and need space for n of them:
struct pixel *pixels = malloc(sizeof(struct pixel) * n);
A useful habit is to write malloc(sizeof(*pixels) * n) instead of repeating the type name. This way, if you ever change the pointer’s type, the allocation stays correct automatically.
malloc Does Not Initialize Memory
The block of memory you get back from malloc contains whatever data happened to be sitting in that region of memory before. These are often called garbage values. Reading from a malloc’d block before writing to it is undefined behavior, meaning your program could crash, produce wrong results, or appear to work fine on your machine and break on someone else’s.
If you need every byte set to zero, C provides calloc as an alternative. calloc takes two arguments (the number of elements and the size of each element) and zeroes out the entire block before returning it. This extra initialization step makes calloc slightly slower than malloc, so if you’re going to fill the memory with your own data immediately, malloc is the better choice.
What Happens Behind the Scenes
When malloc gives you a pointer, it also needs to remember how large that block is so it can be properly freed later. It does this by storing metadata in a small header just before the address it hands back to you. This header typically holds the block size and whether the block is currently allocated. You never see or interact with this header directly, but it’s the reason you should never write before the start of your allocated block: you’d be corrupting malloc’s bookkeeping data.
Checking for Failure
malloc returns NULL when it can’t fulfill your request. The standard guidance for decades has been to check this return value before using the pointer:
int *p = malloc(sizeof(int) * n);
if (p == NULL) { /* handle the error */ }
In practice, the situation is more nuanced than it appears. On most modern operating systems, including macOS, iOS, Android, and FreeBSD, the OS uses a strategy called memory overcommit: it promises memory to your program even if it doesn’t have enough, and only runs into trouble later when the memory is actually used. On these systems, malloc essentially never returns NULL. The only mainstream OS that reliably lets applications detect allocation failures through a NULL return is Windows. Still, checking for NULL is considered good practice, especially if your code needs to be portable or run in constrained environments like embedded systems.
Freeing Memory
Every block you allocate with malloc should eventually be released with free():
free(p);
After calling free, the pointer still holds the old address, but that address is no longer valid. Using a pointer after freeing it (a “use after free” bug) is another form of undefined behavior. A common defensive practice is to set the pointer to NULL immediately after freeing it.
Failing to free memory that’s no longer needed creates a memory leak. In a short-lived program, this might not matter much since the OS reclaims all of a process’s memory when it exits. But in long-running programs like servers, daemons, or anything processing data in a loop, leaked memory accumulates over time and can eventually exhaust system resources.
Resizing With realloc
Sometimes you allocate a block and later realize you need it to be bigger (or smaller). Rather than allocating a new block, copying everything, and freeing the old one yourself, you can use realloc:
p = realloc(p, sizeof(int) * new_size);
realloc attempts to resize the existing block in place. If there isn’t enough room, it allocates a new block of the requested size, copies the old data over, and frees the original. This means the pointer realloc returns may be different from the one you passed in. It can also fail and return NULL, so a safer pattern is to store the result in a temporary pointer first. If you write p = realloc(p, new_size) and it fails, you’ve just lost your only reference to the original block, creating an unrecoverable leak.
The most common use of realloc is building dynamically growing arrays, like reading lines from a file when you don’t know how many there will be.
Common Mistakes
- Forgetting to free memory. This is the most frequent malloc-related bug and the source of memory leaks in C programs.
- Using memory after freeing it. The pointer looks valid, but the memory it points to may have been reassigned. Bugs like this are intermittent and hard to track down.
- Allocating the wrong size. Writing
malloc(sizeof(int *))when you meantmalloc(sizeof(int))allocates enough space for a pointer, not an integer. On 64-bit systems, pointers are 8 bytes while ints are typically 4, so this kind of mistake can silently waste memory or, worse, allocate too little. - Dereferencing without a NULL check. On systems where malloc can fail, using the returned pointer without checking leads to a null pointer dereference, which crashes the program.
Tools like Valgrind and AddressSanitizer can catch many of these errors automatically during development, and they’re worth learning alongside malloc itself.

