Freeing Dynamic Memory the Right Way

Freeing Dynamic Memory the Right Way

Unleashing the Power of Responsible Programming

If you have experience working with the C programming language, you understand the importance of managing memory properly to prevent memory leaks. This comes after dealing with various memory-related errors, such as segmentation faults. In this post, I will focus on freeing dynamically allocated memory. I assume you are familiar with the concept of dynamic memory allocation using functions like malloc(), calloc() and realloc(). We will discuss why this approach is superior and why you should adopt it if you haven't already.

Why Free Memory?

photo view of city with motion effect

Photo Credit: freepik.com

In the dynamic world of programming, where every byte counts and efficiency is the key, the importance of freeing memory in C cannot be overstated. Picture your computer's memory as a bustling city, with each byte playing a crucial role in the seamless execution of your programs. Now, imagine what would happen if these precious bytes were not released back into the system after they've served their purpose – chaos, congestion, and an inevitable slowdown of your program's performance.

The act of freeing memory is akin to maintaining order in this bustling city of bytes. When you allocate memory for a variable or a data structure in C, you're essentially carving out a space in this digital metropolis. However, if you neglect to release that space once it's no longer needed, you're essentially leaving behind abandoned buildings, cluttering up the landscape and slowing down the entire system.

By freeing memory, you're not just practicing good programming hygiene; you're actively contributing to the efficiency and responsiveness of your code. Imagine writing a program that handles large datasets or complex algorithms – without proper memory management, your once nimble and sleek application could transform into a sluggish behemoth, devouring system resources and leaving users frustrated.

Moreover, in C, where manual memory management is the norm, failure to free memory can lead to the notorious memory leaks. These leaks occur when portions of memory are allocated but never deallocated, gradually siphoning off resources and causing your program to become a memory-hogging monster.

Freeing memory isn't just a technical necessity; it's a responsible programming practice. It's about respecting the limited resources of the system and ensuring that your code not only runs efficiently but also plays well with other programs sharing the same memory space.

So, the next time you're knee-deep in code, remember the bustling city of bytes and the importance of keeping it tidy. Freeing memory in C is not just a mundane task; it's a superhero move that unleashes the full potential of your programs, making them faster, more efficient, and ultimately, a joy to run.

A Look at the Original Memory Deallocation Function

Let's take a closer look at the original, basic free() function and observe its behavior in two different scenarios. Firstly, we will use it as is, without taking into account the possibility of dangling pointers. Secondly, we will handle the issue of dangling pointers after freeing dynamic memory.

Naive Memory Deallocation

When freeing dynamic memory, it's common for a programmer to simply trust the free() function to do its job well. This practice usually works fine, except when we try to free memory that has already been freed.

In the code snippet below, we first deallocate memory for the variable str on line 21. This is the usual way to free memory, but there's an issue with this approach. The address received from malloc() on line 12 is still accessible, so when we attempt to free it again on line 30, we end up freeing something that the memory heap has no record of allocating. This results in the infamous "double free" error. This is because, after line 21, str has become a dangling pointer (More on this in the next section).

After compiling and running the program, the address of the str variable is still accessible. This means that there is a possibility of attempting to dereference it, which could cause further issues. However, we will learn how to handle this in the next section.

An image of the compiled and executed program

Avoiding Dangling Pointers

In the same way, as we did in the previous section, we release memory. However, now we take an extra step, which is setting the pointer to NULL. This additional measure helps to prevent dangling pointers, which occur when a pointer refers to a memory location that has already been freed. As you might recall from the previous section on freeing memory without care, the address of the memory location was still accessible even after it was freed. Setting the pointer to NULL after freeing it helps to avoid such issues. The reason why this method works is because the free() function performs no operation when the pointer passed to it is NULL.

In the code snippet provided, there are two important steps. Firstly, we free the memory on line 21 and then set the pointer variable str to NULL on line 30. This prevents a dangling pointer. Thus, if we try to free the memory again on line 38, we will not encounter a "double free" error because the pointer is NULL. The free() function performs no operation when the pointer passed to it is NULL, ensuring that we have avoided any errors.

Here's the result after it is compiled and run.

I have included the relevant lines and output above. The output shows that after the address has been freed, it becomes null (nil). This is how we prevent dangling pointers. Although you can try to free the memory again and again after line 30 without any double-free errors, you wouldn't normally do that. The main lesson here is to always set the pointer to null after freeing the memory it was pointing to.

A Better Way to Free Dynamic Memory

We have learned quite enough about the downfalls of the original plain free() function. In this section, we will discuss a method that I believe is a better approach to freeing dynamically allocated memory. We will first talk about the reason behind it and then implement it. After this, we will add a friendly interface for the function so it's easier to use while being very effective as well.

Combining the Best of Both Worlds

Although the original function performs well, we will enhance it by incorporating error checking and handling dangling pointers.

Here's a breakdown of what we will do.

  1. We will receive the pointer pointing to the memory location needing to be freed.

  2. We check to ensure it is not NULL (valid memory address) before trying to free it.

  3. Once step 2 passes, we free the memory the pointer points to.

  4. As a final step, we set the pointer to NULL to avoid dangling pointers.

This is everything we need to do and we will use the function prototype below. You are free to use any function name that works for you.

#include <stdlib.h>

void _free(void **ptr);

Now let's go ahead and write the code based on everything we have discussed

/**
 * _free - frees dynamic memory allocated through a pointer to 
 * a pointer, providing a safer and more convenient approach.
 *
 * @ptr: a pointer to the pointer pointing to the memory 
 * location to be freed
 *
 * Example usage: `_free((void **)&my_pointer);`
 */
void _free(void **ptr)
{
    /*
     * check if the pointer and the memory it points to 
     * are not NULL before attempting to free
     */
    if (*ptr != NULL && ptr != NULL)
    {
        /* free the memory and set the original pointer to NULL */
        free(*ptr);
        *ptr = NULL;
    }
}

This function takes a pointer to a pointer pointing to a dynamically allocated memory location and frees the memory. It also sets the original pointer to NULL to prevent potential dangling pointers. It is recommended to pass the address of the pointer to ensure proper behavior and avoid undefined behavior when attempting to free an already freed memory block.

In this section, we combined the amazing work of the free() function and added some enhancements to ensure it works better. This function is the best of both worlds, freeing memory and avoiding dangling pointers. Also, it does both in one line when the function is called. Here's an example of usage and results when compiled and run. We are avoiding the "double free" errors in all cases.

A Cleaner Frontend

It's undeniable that the custom function is effective, but it can be troublesome to use for those who forget to typecast every time. The red letters serve as a reminder, but it's still an inconvenience. To make things easier for us and anyone who uses this function, we'll create a better interface for it using function macros. Macros are powerful tools that enable us to have functions that are essentially just macros. They are very useful for many purposes, including ours. The function macro we'll create will perform a simple task.

  1. It will receive the pointer we want to free

  2. It will then call our custom _free() function, passing the address of the pointer it received.

  3. While at step 2, our function-macro handles the typecasting easily and cleanly for us.

Here's the prototype we will use, I've named it safe_free(). Again, you are allowed to use anything that works for you.

/* _free's frontend */
#define safe_free(ptr) _free((void **)&(ptr))

The result of this when compiled and run is the same as before, the only difference now is that we don't worry about typecasting and any of the other things we would have normally done.

Conclusion

I believe this has been informative and useful to you. The need to handle memory judiciously is the job of every responsible software engineer. The more you practice this programming hygiene, the safer and more efficient your solutions will be. Always remember the bustling city of bytes and the importance of keeping it tidy.

All the code snippets are available in my GitHub repo.

Title Photo Credit: Patrick A. Noblet

Have fun in your bustling city of bytes!