Question
In C, programmers are often taught to call free() for every block of memory allocated with malloc(). I want to understand the real consequences of not freeing memory in practice.
For example, consider this program:
#include <stdlib.h>
int main(void)
{
char *a = malloc(1024);
/* Do some arbitrary work with 'a' here. */
return 0;
}
If free(a); is omitted, what actually happens when the program exits? My assumption is that once the process terminates, the operating system reclaims the process memory anyway, so there is no lasting harm. Is that understanding correct?
I also have a second scenario. Suppose a program behaves somewhat like a shell, where users can define variables such as aaa = 123, and those variables are stored in dynamically allocated data structures for later use. In that kind of program, the memory may need to stay allocated for the entire lifetime of the process.
In such a design, is it acceptable for memory to be allocated and kept until the process exits, without being explicitly freed earlier? If that is considered poor design, what is the usual alternative?
Short Answer
By the end of this page, you will understand what happens to malloc()-allocated memory when a C program ends, why free() still matters even if the OS reclaims memory at process termination, and when keeping allocations for the whole program lifetime is a reasonable design choice.
Concept
In C, malloc() asks the runtime for a block of memory on the heap, and free() returns that block so it can be reused while the program is still running.
The key idea is this:
- During program execution, memory you do not
free()remains unavailable for reuse by your program. - When the process terminates, the operating system usually reclaims the process's memory resources.
So for a short-lived program like this:
#include <stdlib.h>
int main(void)
{
char *a = malloc(1024);
return 0;
}
on a normal operating system, the 1024 bytes do not remain permanently lost after the process exits. The OS cleans them up.
However, that does not mean skipping free() is always harmless.
Why free() still matters
1. Leaks matter while the program is alive
If a program runs for a long time, repeated allocations without corresponding frees will steadily increase memory usage.
2. free() expresses ownership and intent
Calling free() documents that the memory is no longer needed. This makes code easier to maintain and less error-prone.
3. Some resources are not just memory
Memory management patterns are often tied to cleanup of other resources too:
- file handles
- sockets
- mutexes
- buffers inside libraries
If code gets used as a model for other cleanup logic, ignoring cleanup can lead to bigger problems.
4. Tools detect leaks
Leak detectors such as Valgrind or sanitizers report memory that was not freed. Even if the OS reclaims it at exit, the leak may still indicate poor lifecycle management.
Is keeping memory until process exit always bad?
No. Sometimes it is a valid design.
If data is meant to exist for the entire lifetime of the program, then allocating it once and freeing it only during final shutdown can be reasonable. For example:
- symbol tables
- configuration data
- caches
- shell variables
- global registries
That is not automatically a bug. The important question is whether the lifetime of the allocation matches the lifetime of the data.
The real rule
A better rule than "free everything immediately" is:
Every allocation should have a clear owner and a clear lifetime.
If the data is needed until the end of the program, freeing it only at shutdown is often fine. If the data is temporary, it should be freed when it is no longer needed.
Mental Model
Think of heap memory like renting storage boxes.
malloc()= rent a box- using the pointer = put things in the box
free()= return the box so you stop paying for it- program exit = the whole warehouse closes and all your boxes are cleared out
If you rent one box and the warehouse closes a second later, not returning the box yourself may not cause visible damage.
But if your program is like a long-running business and keeps renting new boxes without returning old ones, you eventually run out of space or waste resources.
For the shell-variable example, imagine a cabinet of records that must stay available the entire day. You do not throw each record away right after writing it. You keep it as long as it is useful, then discard it when it is no longer needed—or when the office closes.
Syntax and Examples
The core C functions are:
#include <stdlib.h>
void *malloc(size_t size);
void free(void *ptr);
Basic allocation and cleanup
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *buffer = malloc(1024);
if (buffer == NULL) {
return 1;
}
buffer[0] = 'A';
buffer[1] = '\0';
printf("%s\n", buffer);
free(buffer);
return 0;
}
This is the normal pattern:
- allocate memory
- check for
NULL - use the memory
- free it when done
Allocation that lives until program exit
#include <stdlib.h>
static char *global_buffer;
int main(void)
{
global_buffer = malloc(4096);
if (global_buffer == NULL) {
return 1;
}
/* Use global_buffer for the whole program lifetime. */
free(global_buffer);
return 0;
}
If global_buffer is truly needed until the end, freeing it only once near shutdown is a reasonable design.
Shell-like variable storage example
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Variable {
char *name;
char *value;
struct Variable *next;
} Variable;
Variable *create_variable(const char *name, const char *value)
{
Variable *var = malloc(sizeof(Variable));
if (var == NULL) {
return NULL;
}
var->name = malloc(strlen(name) + 1);
var->value = malloc(strlen(value) + 1);
if (var->name == NULL || var->value == NULL) {
free(var->name);
free(var->value);
free(var);
return NULL;
}
strcpy(var->name, name);
strcpy(var->value, value);
var->next = NULL;
return var;
}
void destroy_variable(Variable *var)
{
free(var->name);
free(var->value);
free(var);
}
Even if variables live a long time, it is still useful to have destruction logic. That way you can:
- remove variables later
- clear all variables on shutdown
- test your code cleanly
- avoid accidental long-running leaks
Step by Step Execution
Consider this example:
#include <stdlib.h>
int main(void)
{
char *a = malloc(1024);
if (a == NULL) {
return 1;
}
a[0] = 'X';
a[1] = '\0';
free(a);
return 0;
}
Here is what happens step by step:
-
char *a = malloc(1024);- The program asks for 1024 bytes from the heap.
- If successful,
apoints to the start of that memory block. - If allocation fails,
abecomesNULL.
-
if (a == NULL)- The program checks whether allocation succeeded.
-
a[0] = 'X'; a[1] = '\0';- The program writes into the allocated memory.
-
free(a);- The memory block is returned to the allocator.
- Your program must no longer use
aas if it still points to valid storage.
-
return 0;- The process exits.
- The OS reclaims all process memory mappings and related resources.
Now compare that with this version:
#include <stdlib.h>
int main(void)
{
char *a = malloc(1024);
if (a == NULL) {
return 1;
}
a[0] = 'X';
a[1] = '\0';
return 0;
}
The difference is:
- the allocator never gets the block back during program execution
- but when the process ends, the OS usually reclaims it anyway
So the practical effect in a tiny short-lived program is often negligible. In a long-running process, repeated omissions become real leaks.
Real World Use Cases
This concept shows up in many common situations.
Short-lived command-line programs
A utility that runs for 50 milliseconds and exits may technically leak memory at exit without causing system-wide damage. But it is still better style to clean up.
Long-running servers
A web server, database process, or background worker must free memory that is no longer needed. Otherwise memory usage grows over time.
Interactive shells and REPLs
A shell stores command history, variables, aliases, or parsed state. Some data legitimately lives for the whole session.
Compilers and parsers
Some compilers allocate many small objects while processing one file, then free everything together after the compilation unit is done.
Caches and registries
Programs sometimes intentionally retain memory for quick reuse. That is acceptable if the cache has a limit or matches the application's lifetime.
Embedded systems
In embedded software, memory is often much more constrained. Even small leaks can be serious, especially in software that must run continuously.
Real Codebase Usage
In real projects, developers focus less on "free immediately after malloc" and more on ownership and lifetime.
Common patterns
1. Create/destroy pairs
Functions are often designed in pairs:
Widget *widget_create(void);
void widget_destroy(Widget *w);
This makes memory ownership explicit.
2. Shutdown cleanup
Programs with global state often allocate during startup and release during shutdown.
int app_init(void);
void app_shutdown(void);
3. Container cleanup
If you store data in linked lists, hash tables, or trees, the container usually has a function to free every node.
void map_destroy(Map *map);
4. Early returns with cleanup paths
Real code often uses guard clauses and a shared cleanup section.
char *buf = malloc(1024);
FILE *fp = fopen("data.txt", "r");
if (buf == NULL || fp == NULL) {
free(buf);
if (fp != NULL) fclose(fp);
return 1;
}
/* use resources */
fclose(fp);
free(buf);
5. Arena or pool allocation
Some codebases allocate many related objects from a memory arena and free the whole arena at once. This is common when many objects share the same lifetime.
That is often the structured alternative to manually freeing thousands of tiny allocations one by one.
Common Mistakes
1. Assuming "the OS cleans it up" means leaks do not matter
That is only partly true.
- It is mostly true after process exit.
- It is false while the program keeps running.
Broken thinking:
while (1) {
malloc(1024); /* leak forever */
}
This will eventually exhaust memory.
2. Losing the pointer
If you overwrite the only pointer to an allocation, you cannot free it anymore.
char *p = malloc(100);
p = malloc(200); /* first allocation is now leaked */
free(p);
Avoid this by freeing before overwriting, or by using a separate pointer.
3. Freeing too early
Do not free memory while it is still needed.
char *p = malloc(10);
free(p);
p[0] = 'A'; /* undefined behavior */
After free(p), the pointer must not be used as valid storage.
4. Double free
Freeing the same pointer twice is undefined behavior.
char *p = malloc(10);
free(p);
free(p); /* undefined behavior */
A common defensive habit is:
free(p);
p = NULL;
5. Forgetting nested allocations
If a struct contains dynamically allocated fields, freeing only the struct is not enough.
typedef struct {
char *name;
} User;
User *u = malloc(sizeof(User));
u->name = malloc(50);
free(u); /* leaks u->name */
You must free inner allocations first.
6. Not matching lifetime to design
If data should disappear when a user deletes a variable, keeping it until program exit wastes memory unnecessarily.
Design the cleanup point around when the data stops being useful.
Comparisons
| Situation | Is explicit free() needed? | Why |
|---|---|---|
| Short-lived program allocates once and exits | Usually not required for OS cleanup | The OS typically reclaims memory at process termination |
| Long-running loop keeps allocating | Yes | Memory usage grows during execution |
| Data needed for entire program lifetime | Not necessarily before shutdown | Lifetime matches the process lifetime |
| Data removed dynamically during runtime | Yes | Memory should be reclaimed when no longer needed |
| Library code | Yes | Libraries should not assume when the host process exits |
free() vs process exit cleanup
| Aspect | free() | Process exit cleanup |
|---|---|---|
| Happens during program execution | Yes | No |
| Lets your allocator reuse memory | Yes | No |
| Helps leak detectors report clean shutdown | Yes | Yes, but leaked blocks may still be reported |
| Good for long-running processes | Essential | Not enough |
| Controlled by your code | Yes | Mostly handled by OS/runtime |
Manual freeing vs arena-style cleanup
| Approach | Best for | Trade-off |
|---|---|---|
| Free each object individually | Objects with independent lifetimes | More bookkeeping |
| Free everything at shutdown | Data that lives for entire process | Less flexible |
| Arena/pool allocation | Many related objects with same lifetime | Harder to free individual objects |
Cheat Sheet
void *malloc(size_t size);
void free(void *ptr);
malloc()allocates heap memory.free()releases heap memory for reuse.free(NULL)is safe.- After
free(ptr), do not read or write throughptr. - Setting a pointer to
NULLafterfree()can help avoid accidental reuse. - If a process exits, the OS usually reclaims its memory.
- That does not make leaks harmless in long-running programs.
- Memory that is intentionally kept for the whole program lifetime is not automatically bad design.
- The important rule: each allocation should have a clear owner and lifetime.
Good pattern
char *p = malloc(100);
if (p == NULL) {
return 1;
}
/* use p */
free(p);
p = NULL;
Watch out for
- losing the only pointer
- double free
- use-after-free
- forgetting to free nested allocations
- leaking inside loops
- assuming library or daemon code can rely on process exit for cleanup
FAQ
Is it a memory leak if the OS frees memory when the program exits?
Technically, memory that was never explicitly freed is still considered leaked by many tools, even if the OS reclaims it at process termination.
Is it okay not to free memory in main() right before returning?
In a tiny short-lived program, it often has no practical system impact. But explicit cleanup is still clearer and scales better as the code grows.
Should long-lived data structures be freed before exit?
If they truly live for the whole program lifetime, freeing them only at shutdown is reasonable. It is still helpful to have cleanup functions.
Why do leak-checking tools care if the OS reclaims memory anyway?
Because the tool is checking whether your program manages its own resources correctly, not whether the operating system can clean up after it.
What is the alternative for many related allocations with the same lifetime?
A common alternative is to use a memory pool or arena and free the whole group at once.
Do I need to free memory in a library even if the application will exit soon?
Yes. Library code should manage memory properly and should not assume anything about the host program's lifetime.
Can keeping memory until process exit be good design?
Yes, if the data is intentionally process-lifetime data, such as configuration, symbol tables, or shell session state.
Mini Project
Description
Build a small shell-like variable store in C. The program will let you create variables, update them, print them, and clean them up at the end. This demonstrates the key idea from the question: some data may intentionally live for the entire runtime, but you should still design a clear cleanup path.
Goal
Create a linked-list-based variable store where each variable remains available until explicitly cleared or until program shutdown.
Requirements
- Store variables as name/value pairs using dynamic memory.
- Support adding a new variable and updating an existing one.
- Print all stored variables.
- Free all allocated memory before the program exits.
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.