C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
56: Freeing Memory Correctly
Wait, does free() actually clear the memory or reset the pointer to NULL?
This is a common point of confusion. The short answer is: neither. When you call free(ptr), you aren't wiping the data to zeros, and you certainly aren't changing the value of ptr itself. You're simply telling the memory manager, "I'm done with this specific block of memory; feel free to give it to someone else."
The pointer ptr still holds the exact same memory address it did a millisecond before the call. The data might still be sitting there, too—at least for a little while—until the system overwrites it for another allocation. I've seen plenty of beginners try to "sneak a peek" at the data after freeing it. It might work once, but it's a gamble. You're accessing memory you no longer own, and that's how you end up with those dreaded non-deterministic crashes.
Why the obsession with setting pointers to NULL after freeing them?
You'll see me and other C programmers do this constantly: free(ptr); ptr = NULL;. It feels redundant, but it's a safety net against "use-after-free" bugs. If you accidentally try to use ptr later in a 1,000-line function, and it still points to the old address, your program might not crash immediately. Instead, it'll just read garbage or, worse, overwrite memory that now belongs to a different part of your app. That's a nightmare to debug.
By setting it to NULL, you turn a silent, random bug into a loud, immediate crash (a segmentation fault). I'd much rather have my program crash instantly than have it subtly corrupt a database because of a dangling pointer. Plus, calling free(NULL) is explicitly defined as a no-op in C—it does nothing and it doesn't crash. This means you can safely call free on a pointer without worrying if it's already been freed, provided you nulled it out.
struct User { char *name; int id; }; void delete_user(struct User *u) { // Order matters here! // If we free(u) first, we lose the pointer to u->name. free(u->name); u->name = NULL; free(u); u = NULL; // This only nulls the local copy of the pointer }What happens if I move the pointer and then try to free it?
This is a recipe for disaster. The
free()function requires the exact address that was returned bymalloc(),calloc(), orrealloc(). If you increment your pointer to iterate through an array and then try to free that shifted pointer, the memory manager will have no idea what you're talking about.I once spent an entire afternoon chasing a "heap corruption" error because I was doing something like this:
char *buffer = malloc(100); buffer++; // Moving the pointer to skip a header byte // ... some code ... free(buffer); // CRASH!The heap manager keeps a little bit of metadata just before the address it gives you to track how large the block is. When you pass it a shifted pointer,
free()looks at the memory just before that pointer for the metadata, finds random data instead, and usually panics. If you need to move a pointer for traversal, always keep a separate "anchor" pointer that points to the original start of the allocation.
📋 Practical Task
Exercise: Fixing the Leaky Message Log
You have been handed a piece of code for a "Message Log" that stores a list of strings. However, the current implementation has a massive memory leak and a dangerous double-free bug.
Your task: Modify the clear_log function so that it correctly frees every individual message string before freeing the array of pointers itself. Additionally, ensure that the log_ptr is set to NULL after the cleanup to prevent dangling pointer access.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void clear_log(char **log_ptr, int count) {
// BUG: This currently only frees the array of pointers,
// leaving the actual strings leaked in memory.
// ALSO: It doesn't null out the pointer.
free(log_ptr);
}
int main() {
int msg_count = 3;
char **log = malloc(msg_count * sizeof(char *));
log[0] = strdup("System started");
log[1] = strdup("User logged in");
log[2] = strdup("Connection lost");
printf("Cleaning up log...\n");
clear_log(log, msg_count);
// If clear_log worked and nulled the pointer, this would be safe or easily checkable
if (log == NULL) {
printf("Log successfully cleared.\n");
} else {
printf("Log pointer is still dangling!\n");
}
return 0;
}
Hint: Remember that log is an array of pointers. You need a loop to handle the inner allocations before you handle the outer one.
There are no comments for now.