C Dynamic Memory Allocation
Dynamic memory allocation allows a C program to request memory at runtime instead of determining the complete storage requirement at compile time. The standard library provides malloc, calloc, realloc, and free for managing dynamically allocated storage.
Stack and Heap Memory
Automatic local variables are commonly stored in stack-managed storage, while memory obtained through malloc, calloc, or realloc comes from dynamically allocated storage. The exact implementation of these storage areas is platform-dependent, but the distinction is useful when discussing C memory management.
Why Dynamic Allocation Is Useful
- Allocate memory based on runtime input
- Create dynamically sized arrays
- Build linked data structures
- Resize buffers as data grows
- Manage objects whose lifetime extends beyond a single block
- Avoid unnecessarily large fixed-size arrays
malloc
malloc allocates a specified number of bytes and returns a pointer to the allocated storage. The contents of newly allocated memory are indeterminate.
#include <stdlib.h>
int *values = malloc(10 * sizeof *values);
if (values == NULL) {
return EXIT_FAILURE;
}
Using sizeof with malloc
Using sizeof on the pointed-to object makes allocation expressions easier to maintain when the pointer type changes.
int *values = malloc(10 * sizeof *values);
malloc Does Not Initialize Memory
The bytes returned by malloc have indeterminate values. Reading an object before storing an appropriate value into it can result in undefined behavior.
int *value = malloc(sizeof *value);
if (value != NULL) {
*value = 42;
}
calloc
calloc allocates storage for an array of objects and initializes all bytes of the allocated storage to zero.
int *values = calloc(10, sizeof *values);
if (values == NULL) {
return EXIT_FAILURE;
}
malloc vs calloc
| Function | Purpose | Initialization |
|---|---|---|
| malloc | Allocate a specified number of bytes | No initialization of allocated storage |
| calloc | Allocate an array of objects | All allocated bytes are initialized to zero |
free
free releases dynamically allocated storage back to the implementation.
int *values = malloc(10 * sizeof *values);
if (values != NULL) {
/* use values */
free(values);
}
A Pointer After free
Calling free does not automatically change the pointer variable to NULL. The pointer becomes invalid for accessing the released object.
free(values);
values = NULL;
Setting the pointer to NULL can prevent accidental reuse of that particular pointer variable.
free(NULL)
Calling free with a null pointer has no effect.
int *values = NULL;
free(values);
realloc
realloc changes the size of a previously allocated object. It may extend the existing allocation or move the object to a new location.
int *values = malloc(10 * sizeof *values);
if (values == NULL) {
return EXIT_FAILURE;
}
int *tmp = realloc(values, 20 * sizeof *values);
if (tmp == NULL) {
free(values);
return EXIT_FAILURE;
}
values = tmp;
Why realloc Should Use a Temporary Pointer
If realloc fails, it returns NULL while the original allocation remains valid. Assigning realloc directly to the only pointer would lose the original address and cause a memory leak.
int *tmp = realloc(values, new_count * sizeof *values);
if (tmp != NULL) {
values = tmp;
}
realloc with Size Zero
Requests involving a zero size have special implementation-defined or implementation-specific historical behavior depending on the C standard version and implementation. Portable code should avoid relying on realloc(ptr, 0) as a substitute for an explicit free.
Growing a Dynamic Array
#include <stdlib.h>
int main(void)
{
size_t capacity = 4;
size_t count = 0;
int *values = malloc(capacity * sizeof *values);
if (values == NULL) {
return EXIT_FAILURE;
}
for (int i = 0; i < 20; ++i) {
if (count == capacity) {
size_t new_capacity = capacity * 2;
int *tmp = realloc(values,
new_capacity * sizeof *values);
if (tmp == NULL) {
free(values);
return EXIT_FAILURE;
}
values = tmp;
capacity = new_capacity;
}
values[count++] = i;
}
free(values);
return EXIT_SUCCESS;
}
Allocation Failure
malloc, calloc, and realloc can fail to allocate the requested storage. A failed allocation returns NULL, so programs must check the result before dereferencing it.
char *buffer = malloc(1024);
if (buffer == NULL) {
/* Handle allocation failure */
return EXIT_FAILURE;
}
Integer Overflow in Allocation Sizes
Calculating the number of bytes with multiplication can overflow size_t before the allocation function is called.
size_t count = 1000000;
size_t element_size = sizeof(int);
if (count > SIZE_MAX / element_size) {
return EXIT_FAILURE;
}
int *values = malloc(count * element_size);
For allocation calculations involving untrusted or very large sizes, check multiplication overflow before calling an allocation function.
Dynamic Arrays
A dynamically allocated block can be accessed using normal array indexing because allocated storage can be used through a pointer to the appropriate object type.
size_t count = 100;
int *values = malloc(count * sizeof *values);
if (values != NULL) {
values[0] = 10;
values[99] = 20;
free(values);
}
Dynamic Strings
Dynamically allocated storage is useful when a string's required size is determined at runtime.
const char *source = "Hello, C!";
size_t length = strlen(source);
char *copy = malloc(length + 1);
if (copy != NULL) {
memcpy(copy, source, length + 1);
free(copy);
}
The additional byte is required for the terminating null character.
Including string.h
#include <string.h>
Memory Ownership
Every dynamically allocated object should have clearly defined ownership: code should know who is responsible for eventually calling free.
Ownership Transfer
A function can transfer ownership of dynamically allocated memory to its caller.
char *create_message(void)
{
char *message = malloc(32);
if (message == NULL) {
return NULL;
}
strcpy(message, "Hello");
return message;
}
/* Caller owns the returned memory. */
Memory Leaks
A memory leak occurs when dynamically allocated storage is no longer reachable but has not been released.
int *values = malloc(100 * sizeof *values);
/* Pointer is lost without calling free. */
values = NULL;
Repeated leaks can cause a program's memory usage to grow unnecessarily.
Dangling Pointers
A dangling pointer refers to storage whose lifetime has ended.
int *value = malloc(sizeof *value);
if (value != NULL) {
*value = 42;
free(value);
/* value is now a dangling pointer. */
}
Use-After-Free
Accessing an object after its allocated storage has been released results in undefined behavior.
free(value);
/* Undefined behavior. */
printf("%d\n", *value);
Double Free
Passing the same allocation to free more than once results in undefined behavior.
free(value);
free(value); /* Undefined behavior */
Invalid free
Only pointers returned by the appropriate allocation functions, or a null pointer, may be passed to free. A pointer to an automatic object must not be freed.
int value = 42;
free(&value); /* Invalid */
Interior Pointers
A pointer to the middle of an allocated block is not itself a pointer that can be passed to free.
char *buffer = malloc(100);
if (buffer != NULL) {
char *middle = buffer + 10;
free(middle); /* Invalid */
free(buffer);
}
Matching Allocation and Deallocation
Memory obtained from malloc, calloc, or realloc is released with free. C does not provide a separate delete operator like C++.
realloc Can Move Memory
After a successful realloc that changes the object's address, pointers into the old allocation become invalid because the old storage is released.
int *values = malloc(10 * sizeof *values);
if (values != NULL) {
int *element = &values[3];
int *tmp = realloc(values, 100 * sizeof *values);
if (tmp != NULL) {
values = tmp;
/* element may now be invalid. */
}
}
Avoiding Stale Pointers After realloc
After a successful realloc, recompute pointers into the allocation instead of assuming previous interior pointers remain valid.
Dynamic Structures
Dynamic allocation is fundamental for structures such as linked lists, trees, graphs, hash tables, and dynamically sized buffers.
struct Node {
int value;
struct Node *next;
};
struct Node *node = malloc(sizeof *node);
if (node != NULL) {
node->value = 42;
node->next = NULL;
}
Allocating a Struct Correctly
struct Node *node = malloc(sizeof *node);
Using sizeof *node avoids repeating the structure type in the allocation expression.
Freeing a Linked Structure
struct Node *current = head;
while (current != NULL) {
struct Node *next = current->next;
free(current);
current = next;
}
Nested Allocations
If a dynamically allocated object owns other dynamically allocated objects, all owned allocations must eventually be released.
struct Person {
char *name;
};
struct Person *person = malloc(sizeof *person);
if (person != NULL) {
person->name = malloc(32);
if (person->name == NULL) {
free(person);
return EXIT_FAILURE;
}
free(person->name);
free(person);
}
Partial Allocation Failure
When several allocations are required to construct one object, failure partway through construction requires cleanup of everything that was already allocated.
Cleanup Pattern
char *first = NULL;
char *second = NULL;
first = malloc(100);
if (first == NULL) {
return EXIT_FAILURE;
}
second = malloc(200);
if (second == NULL) {
free(first);
return EXIT_FAILURE;
}
/* Use both allocations. */
free(second);
free(first);
calloc and Zero Values
calloc initializes allocated bytes to zero. For integer types, an all-bits-zero representation is guaranteed to represent zero for the standard integer types. For arbitrary pointer or floating-point objects, do not generally assume that zeroing raw bytes is equivalent to assigning the C value 0.
Flexible Array Members
A flexible array member can be used with dynamic allocation to store a structure and additional variable-sized data in one allocation.
struct Buffer {
size_t length;
char data[];
};
size_t length = 100;
struct Buffer *buffer = malloc(sizeof *buffer + length);
if (buffer != NULL) {
buffer->length = length;
free(buffer);
}
Flexible Array Member Overflow
When calculating the allocation size for a flexible array member, check arithmetic for overflow before performing the allocation.
Alignment
Memory returned by the standard allocation functions is suitably aligned for any object type with a fundamental alignment requirement.
Allocated Memory and Lifetime
The lifetime of allocated storage begins when allocation succeeds and ends when the storage is released or otherwise becomes unavailable according to the applicable allocation operation.
Allocation Is Not Garbage Collection
C does not automatically determine when dynamically allocated objects are no longer needed. The programmer or an explicit ownership mechanism must ensure that allocated memory is released.
Memory Management APIs Beyond the C Standard
Operating systems and runtime libraries often provide additional allocation facilities. Examples include aligned allocation interfaces and debugging allocators. These APIs are platform-specific and should not be confused with the ISO C allocation functions.
Aligned Allocation
Modern C standards provide aligned_alloc for requesting storage with a specified alignment, subject to its size and alignment requirements.
#include <stdlib.h>
void *memory = aligned_alloc(64, 1024);
if (memory != NULL) {
/* use memory */
free(memory);
}
Memory Debugging
Dynamic-memory bugs can be difficult to diagnose because their effects may appear long after the original mistake. Memory debugging tools can detect leaks, invalid accesses, use-after-free errors, and double frees.
Common Dynamic Memory Bugs
- Memory leaks
- Use-after-free
- Double free
- Freeing a non-allocated pointer
- Buffer overflows
- Integer overflow in allocation sizes
- Losing the original pointer after realloc failure
- Using stale pointers after realloc moves an allocation
- Forgetting to free nested allocations
- Dereferencing NULL after allocation failure
Best Practices
- Check every allocation that can fail
- Use sizeof *pointer for allocation sizes
- Use a temporary pointer with realloc
- Define ownership clearly
- Free each allocation exactly once
- Set pointers to NULL when doing so improves safety
- Check arithmetic for allocation-size overflow
- Clean up partially constructed objects on failure
- Avoid keeping stale interior pointers across realloc
- Use memory-analysis tools during development
- Keep allocation and deallocation responsibilities easy to identify
Quick Reference
| Function | Purpose |
|---|---|
| malloc | Allocate a specified number of bytes |
| calloc | Allocate an array and zero all allocated bytes |
| realloc | Resize an existing allocation |
| free | Release dynamically allocated storage |
| aligned_alloc | Allocate storage with a requested alignment |
Practice Exercises
- Allocate an integer array whose size is entered by the user
- Initialize a dynamic array using calloc
- Resize an array with realloc as new values are added
- Write a function that dynamically creates a string copy
- Implement a dynamically growing character buffer
- Create and destroy a linked list using dynamic allocation
- Handle allocation failure during construction of a nested structure
- Demonstrate the difference between a memory leak and a dangling pointer
- Write an allocation-size overflow check
- Implement a structure containing a flexible array member
- Track ownership of dynamically allocated objects in a small program
- Use a memory debugging tool to find a deliberate leak
Conclusion
Dynamic memory allocation gives C programs control over storage whose size and lifetime are determined at runtime. malloc, calloc, realloc, and free form the core of standard C dynamic memory management.
That control comes with responsibility. Correct programs must handle allocation failures, prevent leaks and dangling pointers, avoid double frees and buffer overflows, and establish clear ownership rules. Good allocation patterns make dynamic C programs significantly safer and easier to maintain.