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
58: Using Valgrind to Detect Memory Errors
Up until now, we've been trusting our instincts with malloc and free. In a small program, that usually works. But as soon as your projects grow, "hoping" you freed every pointer is a recipe for a midnight debugging session when your server crashes because it ran out of RAM. This is where Valgrind comes in.
Valgrind isn't a compiler; it's a tool that runs your compiled binary in a virtual environment to track every single byte of memory your program touches. Specifically, we'll be using the Memcheck tool, which is the gold standard for finding leaks and "use-after-free" bugs.
Building a Leaky String Array
Let's build a small utility that takes a number of entries and allocates a list of strings. I want to simulate a simple database record fetch. I'll write this quickly, and I'll intentionally leave a gap in my logic so we have something to find.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void process_records(int count) {
// Allocate the array of pointers
char **records = malloc(count * sizeof(char *));
for (int i = 0; i < count; i++) {
// Allocate each individual string
records[i] = malloc(20 * sizeof(char));
sprintf(records[i], "Record %d", i);
printf("Processed: %s\n", records[i]);
}
// Cleanup phase
for (int i = 0; i < count; i++) {
free(records[i]);
}
// I'm intentionally forgetting to free(records) here.
}
int main() {
process_records(5);
return 0;
}
To the naked eye, this looks mostly correct. I'm freeing the individual strings inside the loop. If I run this, it finishes instantly and prints the output perfectly. The OS will reclaim the memory once the process exits, so you won't see your system RAM vanish, but in a long-running app, this is a slow-motion train wreck.
Running the Memcheck Tool
Before we run Valgrind, you must compile with the -g flag. If you don't, Valgrind can tell you that memory leaked, but it can't tell you where in the source code it happened because the debugging symbols are missing.
gcc -g -o record_test record_test.c
valgrind --leak-check=full ./record_test
I use --leak-check=full because the default settings are often too vague. I want to see exactly which allocation wasn't freed.
Interpreting the "Definitely Lost" Report
When I run the command above, I get a wall of text. Don't let it intimidate you; look for the "LEAK SUMMARY" at the bottom. I'm seeing this:
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
"Definitely lost" means the pointer to that memory is gone, and there is absolutely no way to free it now. If I scroll up, Valgrind gives me a stack trace:
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==12345== by 0x4005B7: process_records (record_test.c:7)
==12345== by 0x400621: main (record_test.c:23)
There it is. Line 7. That's where I allocated the records array. Valgrind is telling me that while I freed the contents of the array (the strings), I never freed the array itself.
Fixing the Leak and Verifying the Clean Slate
Now I'll go back and add free(records); after my cleanup loop. It's a simple fix, but this is exactly how I catch bugs in larger projects where the allocation might happen in one file and the free is supposed to happen in another.
After recompiling and running Valgrind again, I'm looking for the "Golden Ticket" of C programming:
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
When you see that, you can actually sleep at night.
📋 Practical Task
Exercise: Fixing the Leaky User Directory
You've been handed a piece of code that manages a "User Directory." The program allocates a structure for a user and a dynamic string for their username, but it's currently leaking memory. Your goal is to use Valgrind to identify the leak and fix it.
The Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *username;
int id;
} User;
void create_user(int id, const char *name) {
User *u = malloc(sizeof(User));
u->id = id;
u->username = malloc(strlen(name) + 1);
strcpy(u->username, name);
printf("User %d (%s) created.\n", u->id, u->username);
// The developer attempted to clean up here
free(u);
}
int main() {
create_user(101, "dev_mentor");
create_user(102, "c_student");
return 0;
}
Your Task:
- Compile the program with
-g. - Run it through Valgrind with
--leak-check=full. - Identify which specific allocation is being "definitely lost."
- Modify the
create_userfunction so that Valgrind reports "All heap blocks were freed."
There are no comments for now.