C Dynamic Memory (malloc)
Dynamic memory lets a C program request storage while it is running instead of deciding every size at compile time. The main function for this is malloc, which asks the heap for a block of bytes and returns a pointer to the first byte. This matters whenever the amount of data depends on input, a file, a network response, or a calculation that is not known when the program is built.
Overview: How Dynamic Memory Works
C has several common storage areas. Local variables usually live in automatic storage, often called the stack, and they disappear when the block or function returns. String literals and globals live elsewhere for the lifetime of the program. Dynamic memory lives in allocated storage, often called the heap. You request it explicitly with library functions from <stdlib.h>, use it through a pointer, and release it explicitly with free.
malloc does not know about int, double, structures, or arrays. It receives a byte count of type size_t. If it succeeds, it returns a void *, which can be assigned to an object pointer such as int * or struct Student *. If it cannot satisfy the request, it returns NULL. In C, you do not need to cast the result of malloc; including <stdlib.h> gives the compiler the proper declaration.
The memory returned by malloc is uninitialized. Its bytes contain indeterminate values, so reading them before writing your own values is a bug. If you need zero-initialized memory, use calloc or initialize the block yourself. The lifetime of a dynamically allocated block continues until you call free on the pointer returned by the allocation function, or until the program ends. The pointer variable can go out of scope before the block is freed, which creates a memory leak because the program has lost the address needed to release it.
Syntax
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 3;
int *items = malloc(count * sizeof *items);
if (items == NULL) {
printf("Allocation failed\n");
return 1;
}
for (size_t i = 0; i < count; i++) {
items[i] = (int)(i + 1);
}
printf("allocated %zu integers\n", count);
free(items);
return 0;
}
Output:
allocated 3 integers
| Part | Meaning |
|---|---|
#include <stdlib.h> |
Declares malloc, calloc, realloc, and free. |
count * sizeof *items |
Computes the byte count for count objects of the pointed-to type. |
items == NULL |
Checks whether allocation failed before the pointer is used. |
free(items) |
Returns the block to the allocator. The pointer value must not be dereferenced afterward. |
Examples
Allocate an array whose size is known at run time
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t count = 5;
int *numbers = malloc(count * sizeof *numbers);
if (numbers == NULL) {
printf("Could not allocate memory\n");
return 1;
}
for (size_t i = 0; i < count; i++) {
numbers[i] = (int)((i + 1) * (i + 1));
}
printf("numbers:");
for (size_t i = 0; i < count; i++) {
printf(" %d", numbers[i]);
}
printf("\n");
free(numbers);
return 0;
}
Output:
numbers: 1 4 9 16 25
This program allocates enough space for five int objects, writes each element, prints the array, and frees the block. The expression sizeof *numbers follows the pointer type, so if numbers later becomes a long *, the allocation size still stays correct.
Allocate space for a copied string
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
const char *source = "dynamic memory";
char *copy = malloc(strlen(source) + 1);
if (copy == NULL) {
printf("Could not allocate string copy\n");
return 1;
}
strcpy(copy, source);
printf("Original: %s\n", source);
printf("Copy: %s\n", copy);
printf("Characters copied: %zu\n", strlen(copy));
free(copy);
return 0;
}
Output:
Original: dynamic memory
Copy: dynamic memory
Characters copied: 14
C strings require one extra byte for the terminating null character ' '. The allocation uses strlen(source) + 1, then strcpy copies both the visible characters and the terminator. Without the extra byte, the copy would write past the end of the allocation.
Allocate an array of structures
#include <stdio.h>
#include <stdlib.h>
struct Student {
const char *name;
int score;
};
int main(void) {
size_t count = 3;
struct Student *students = malloc(count * sizeof *students);
if (students == NULL) {
printf("Could not allocate students\n");
return 1;
}
students[0] = (struct Student){"Ada", 92};
students[1] = (struct Student){"Ken", 85};
students[2] = (struct Student){"Lin", 97};
int total = 0;
printf("%zu students\n", count);
for (size_t i = 0; i < count; i++) {
printf("%s: %d\n", students[i].name, students[i].score);
total += students[i].score;
}
printf("Average score: %.2f\n", total / (double)count);
free(students);
return 0;
}
Output:
3 students
Ada: 92
Ken: 85
Lin: 97
Average score: 91.33
Dynamic memory is not limited to simple values. Here one allocation creates room for three struct Student objects. Each element is assigned with a compound literal, then the array is processed just like a normal array. The important difference is ownership: this function owns the allocated block and must call free exactly once.
Grow an array with calloc and realloc
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int inputs[] = {4, 8, 15, 16, 23, 42, 108};
size_t input_count = sizeof inputs / sizeof inputs[0];
size_t count = 0;
size_t capacity = 2;
int *values = calloc(capacity, sizeof *values);
if (values == NULL) {
printf("Initial allocation failed\n");
return 1;
}
for (size_t i = 0; i < input_count; i++) {
if (count == capacity) {
size_t new_capacity = capacity * 2;
int *larger = realloc(values, new_capacity * sizeof *values);
if (larger == NULL) {
free(values);
printf("Resize failed\n");
return 1;
}
values = larger;
capacity = new_capacity;
}
values[count] = inputs[i];
count++;
}
printf("count=%zu capacity=%zu\n", count, capacity);
printf("values:");
for (size_t i = 0; i < count; i++) {
printf(" %d", values[i]);
}
printf("\n");
free(values);
return 0;
}
Output:
count=7 capacity=8
values: 4 8 15 16 23 42 108
calloc takes a count and an element size, allocates the total space, and initializes all bits to zero. realloc asks the allocator to resize an existing block. It may extend the block in place, or it may allocate a new block, copy the old contents, free the old block, and return a different address. This example stores the return value in larger first; assigning directly to values would lose the original block if resizing failed.
How It Works Step by Step
- The program computes how many bytes it needs. For arrays, this is usually
count * sizeof *pointer. - The allocator searches its managed heap area for a block large enough to satisfy the request. It may ask the operating system for more memory behind the scenes.
- If successful,
mallocreturns an address aligned suitably for any object type that fits in the block. If not, it returnsNULL. - Your program treats that address as the start of an array of the intended type. Pointer arithmetic such as
items[i]moves in units of that type, not raw bytes. - When the block is no longer needed,
freemarks it available for reuse. The bytes may still appear to contain old data, but the program no longer owns them.
After free, every pointer that still stores that address is a dangling pointer. The pointer variable itself still exists, but the allocation does not belong to your program anymore. Dereferencing it, freeing it again, or using it as an array is undefined behavior.
Common Mistakes
Using the wrong allocation size
A common mistake is allocating count * sizeof pointer instead of count * sizeof *pointer. The first version uses the size of the pointer variable, not the size of the element. Sometimes that accidentally allocates too much; sometimes it allocates too little. Prefer malloc(count * sizeof *items) because the expression stays tied to the pointed-to type.
Forgetting to check for failure
Do not write to the result of malloc until you know it is not NULL. On many small examples allocation almost always succeeds, but real programs can fail because of huge inputs, memory limits, or integer overflow in size calculations. A clean error path should free anything already allocated and return an error code or print a useful message.
Using memory after free
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *score = malloc(sizeof *score);
if (score == NULL) {
printf("Allocation failed\n");
return 1;
}
*score = 42;
printf("value before free: %d\n", *score);
free(score);
score = NULL;
if (score == NULL) {
printf("pointer cleared\n");
}
return 0;
}
Output:
value before free: 42
pointer cleared
The value is printed before free. After freeing the block, the program sets the pointer to NULL and only tests the pointer itself. Setting a pointer to NULL is not required, but it helps prevent accidental reuse in longer functions. It does not clear other copies of the same address, so ownership still has to be designed carefully.
Losing memory when realloc fails
The expression values = realloc(values, new_size) is risky because failure returns NULL while the original allocation remains allocated. If you overwrite the only pointer with NULL, you leak the old block. Store the result in a temporary pointer, check it, and only then replace the original pointer.
Best Practices
- Include
<stdlib.h>whenever you use allocation functions. - Use
sizeof *pointerrather than repeating the element type in the allocation expression. - Check for
NULLbefore reading from or writing to allocated memory. - Initialize allocated memory before reading it; use
callocwhen zero initialization is the right default. - Pair each successful allocation with exactly one
freeon every control path. - Free in the reverse order of ownership when several allocations depend on each other.
- Use a temporary pointer for
reallocso the original block is not lost on failure. - For multiplication sizes, consider whether
count * sizeof *pcould overflowsize_twhencountcomes from untrusted input. - Do not cast the return value of
mallocin C; the cast can hide a missing declaration in older build modes.
Practice Exercises
- Write a program that allocates an array of
double, fills it with three temperatures, prints the average, and frees the array. - Write a function
char *repeat_char(char ch, size_t count)that returns a newly allocated string containingcountcopies ofchplus a null terminator. ReturnNULLif allocation fails. - Modify the growing-array example so it reads integers with
scanfuntil input ends, expanding the array as needed, then prints the minimum and maximum.
Summary
mallocrequests a byte-sized block of dynamic storage and returns a pointer, orNULLon failure.- Dynamically allocated memory is uninitialized unless you use
callocor initialize it yourself. - The safest array allocation pattern is
pointer = malloc(count * sizeof *pointer). freeends your ownership of a block; using the pointer afterward is undefined behavior.realloccan move a block, so assign its result to a temporary pointer until you know it succeeded.- Good C programs make ownership obvious: the code that allocates memory must have a clear plan for releasing it.
