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
88: Dynamic Memory Functions Revisited
A few years ago, I was reviewing code for a junior dev who was writing a custom log parser. He had a loop that read characters from a file one by one, growing a buffer with realloc every time he hit the limit. On his machine, with a few small test files, it was lightning fast. But when we pushed it to a staging server with a 2GB log file, the program started leaking memory like a sieve and eventually crashed with a segfault. When we looked at the code, he had written buffer = realloc(buffer, new_size);. He didn't realize that if realloc fails, it returns NULL, but it doesn't free the original block of memory. By assigning that NULL directly back to his only pointer, he lost the address of the original block forever. He'd created a leak and a crash in a single line of code.
The Proper Way to Resize
We've touched on malloc and free before, but realloc is where things get dangerous. The golden rule is: never assign the return value of realloc directly to your original pointer. If the system can't find a contiguous block of memory large enough to accommodate the new size, it won't touch the old block; it just gives you a NULL. If you've already overwritten your pointer with that NULL, you can't free the old memory, and you can't access your data.
Instead, you should always use a temporary pointer. I usually do it like this:
void *tmp = realloc(original_ptr, new_size);
if (tmp == NULL) {
// Handle the error: the original_ptr is still valid here!
// You can decide to free it and exit, or try to save the data.
perror("realloc failed");
free(original_ptr);
exit(EXIT_FAILURE);
}
original_ptr = tmp;
It's a bit more verbose, but it's the only way to ensure your program remains stable under memory pressure. I've spent far too many late nights debugging "random" crashes that were actually just failed reallocations in a high-load environment.
Growth Strategies and Fragmentation
Another thing I noticed in that log parser was that the dev was increasing the buffer size by exactly one byte every time he needed more room. This is a performance killer. Every call to realloc potentially involves searching for a new memory block and copying all your existing data to that new location. If you do this for every single character in a million-line file, you're spending more time copying memory than actually parsing logs.
The industry standard is "exponential growth." Usually, you double the capacity when you run out of space. It might feel wasteful to allocate more than you need, but the trade-off is massive. You move from an O(n²) time complexity for building your buffer down to O(n). If you start at 16 bytes and double it each time, you'll only call realloc a handful of times even for very large strings. Just remember to track both the capacity (how much memory you have) and the length (how much you're actually using).
Cleaning Up the Aftermath
Finally, let's talk about the "Dangling Pointer." You know you have to free your memory, but simply calling free(ptr) doesn't actually erase the address stored in ptr. The pointer still points to that memory location; it's just that you no longer own it. This is how "use-after-free" bugs happen, which are a primary source of security vulnerabilities in C.
I've developed a habit of immediately setting a pointer to NULL after freeing it. It's a simple safety net. If you accidentally try to use that pointer later, the program will crash immediately with a null pointer dereference. While a crash sounds bad, a predictable crash at the point of failure is a thousand times easier to debug than a silent memory corruption that happens three functions later because you wrote data into a block of memory that the heap manager had already reassigned to something else.
š Practical Task
Building a Resizable Integer Vector with Safe Growth
Your task is to implement a simple dynamic array (a "vector") that can store integers. You need to create a structure that keeps track of the array pointer, the current number of elements, and the total capacity.
Requirements:
- Implement a function
int add_element(Vector *v, int value). - The vector should start with an initial capacity of 4.
- When the vector is full, use
reallocto double the capacity. - You must use a temporary pointer for
reallocto prevent memory leaks on failure. - Implement a
cleanup_vector(Vector *v)function that frees the memory and sets the internal pointer toNULL. - In your
mainfunction, add 20 integers to the vector to trigger multiple growth cycles, print the final capacity to verify it doubled correctly, and then clean up the memory.
There are no comments for now.