Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 23

Memory Management: Freeing Memory and Preventing Leaks

Recommended resources

Hello! Welcome back to our series on C pointers.

In our last lesson, we explored realloc() and saw how crucial it is to handle its potential failure. We learned that naively overwriting your pointer with the result of realloc could lead to a memory leak if the function fails. This was our first formal encounter with one of the most notorious problems in C programming.

Today, we're going to tackle this issue head-on. This lesson is dedicated to understanding how to properly deallocate memory using free() and explaining the profound importance of preventing memory leaks. For a developer in the automotive domain, where systems must run reliably for years, mastering memory deallocation isn't just good practice—it's a core requirement for building safe and stable software.

The Cardinal Rule of Dynamic Memory

The heap gives you great power to allocate memory as needed, but it comes with one non-negotiable responsibility: you must return what you borrow.

For every block of memory you acquire using malloc(), calloc(), or a successful realloc(), you must explicitly release it once you are done with it. This is done using the free() function, declared in <stdlib.h>:

void free(void *ptr);

The function takes a single argument: a pointer that holds the starting address of a dynamically allocated memory block. It returns this block to the heap manager, making the memory available for future allocations.

Failing to do this results in a memory leak.

To understand this from a perspective you're familiar with, let's explore a definition tailored for embedded systems engineers.

Understanding Memory Leaks in Embedded Systems and ...

This article, 'Understanding Memory Leaks in Embedded Systems', provides a concise and highly relevant overview of memory leaks and their primary causes. It frames the problem within the context of resource-constrained, long-running systems.

Please read the sections titled 'What Are Memory Leaks?', 'Key Characteristics of Memory Leaks', and the first two points under 'Causes of Memory Leaks in Embedded Systems' ('Improper Memory Deallocation' and 'Lost References'). Focus on how these concepts apply to systems that must operate without interruption.

As the article highlights, a memory leak occurs when your program allocates memory but fails to release it. This "lost" memory can't be used by your program again, nor can it be given to other parts of the system.


How Memory Gets "Leaked"

There are two main scenarios that lead to memory leaks:

  1. Simple Negligence: The most straightforward cause is simply forgetting to call free() on a pointer to a memory block that is no longer needed.
  2. Losing the Pointer: This is a more subtle, but very common, cause. If the only pointer variable holding the address of a heap-allocated block is destroyed (e.g., it goes out of scope) or overwritten, that block of memory becomes an "orphan." There is no longer any way for the program to refer to it, and thus no way to free it.

This second scenario is critical to understand. The memory isn't "gone"; it's still marked as "in-use" by the heap manager, but your program has lost the key—the address—to access or return it.

The following article gives a clear example of this happening when a function that allocates memory returns.

Understanding Memory Management, Part 1: C

The article 'Understanding Memory Management, Part 1: C' contains an excellent section that illustrates how a memory leak would occur if cleanup code is omitted.

Read the section titled 'Cleaning Up'. Pay close attention to the paragraph that begins with 'If this function were to return without cleaning up, it would create what's called a "memory leak"'. This explains how the lines pointer, being a stack variable, would be lost upon function return, orphaning the heap memory it points to.


The Real-World Impact of Memory Leaks

In a short-lived desktop application, a small memory leak might go unnoticed. When the program terminates, the operating system reclaims all of its memory anyway.

However, in an automotive ECU, a server, or any long-running embedded system, the story is completely different. A leak in a function that is called repeatedly—say, every time a CAN message is processed or a sensor reading is logged—will cause the system's available memory to shrink steadily over time.

Visualizing Memory Leaks in Heap Memory
This diagram illustrates the effect of a memory leak over time. Initially, the system has normal memory usage. As unreleased blocks accumulate, available memory dwindles, leading to poor performance. Eventually, the heap is exhausted, and the system can no longer allocate memory, often resulting in a crash.

This gradual exhaustion of memory leads to performance degradation and, ultimately, a crash. This is not just a bug; in the context of functional safety (ISO 26262), it's a critical hazard.

The following video provides a powerful demonstration of this process. You'll see a program deliberately written with a memory leak and watch its memory consumption grow until it fails.

Memory Leaks And How To Prevent Them | C Programming Tutorial

The video 'Memory Leaks And How To Prevent Them' offers a fantastic visual demonstration of a memory leak in action.

Please watch from 04:08 to 11:23. This segment covers: The consequence of allocating in a loop without freeing (04:08 - 07:52). Using a system utility to watch the program's memory usage grow (07:52 - 10:05). How adding free() solves the problem completely (10:05 - 11:23). The other cause of leaks: losing the pointer itself.


Preventing Leaks: Best Practices

Now that you understand the danger, let's focus on the discipline required to prevent leaks.

1. The free() Function and Dangling Pointers

It is crucial to understand what free(ptr) does and does not do:

  • It does: Tell the heap manager that the memory block at the address stored in ptr can be reused.
  • It does not: Change the value of the ptr variable itself.

After free(ptr) is called, the pointer ptr still holds the same memory address. However, that memory location is no longer allocated to your program. The pointer is now a dangling pointer. Attempting to read from or write to it results in undefined behavior. We will explore this specific error, known as "use-after-free," in detail in our next lesson.

A small but important detail: calling free(NULL) is guaranteed to be safe and does nothing. This simplifies cleanup code, as you don't need to check if a pointer is NULL before freeing it.

2. Ensuring Cleanup in All Code Paths

A common source of leaks in complex functions is an early return for error handling. If you allocate memory at the start of a function and then hit an error condition and return midway through, you might skip the free() call at the end.

To write robust code, you must ensure that allocated resources are cleaned up, no matter how the function terminates. A common, if debated, pattern in C for achieving this is using goto to a single cleanup block at the end of the function.

Understanding Memory Management, Part 1: C

Let's revisit 'Understanding Memory Management, Part 1: C' to see a practical pattern for handling errors without leaking memory.

Read the section 'Error Handling'. It shows how a simple return can leak memory and introduces the goto error; pattern to consolidate all cleanup logic in one place. This is a prevalent idiom in large-scale C projects like the Linux kernel.

Test your understanding!

You are writing a function for an ADAS system that processes incoming sensor data packets. The function allocates a buffer to parse the packet payload. The function has several validation checks, and it might exit early if a malformed packet is detected.

Here is a flawed version of the function:

#include <stdlib.h>
#include <stdio.h>

#define MAX_PAYLOAD_SIZE 256

// Represents a data packet
typedef struct {
    int id;
    int payload_len;
    // ... other fields
} PacketHeader;

int process_packet(PacketHeader* header) {
    if (header == NULL) {
        printf("Error: Null header.\n");
        return -1; // Error
    }

    if (header->payload_len <= 0 || header->payload_len > MAX_PAYLOAD_SIZE) {
        printf("Error: Invalid payload length.\n");
        return -1; // Error
    }

    // Allocate buffer for payload
    char* payload_buffer = (char*)malloc(header->payload_len);
    if (payload_buffer == NULL) {
        printf("Error: Payload buffer allocation failed.\n");
        return -1; // Error
    }
    
    // ...
    // Code to read payload into payload_buffer would go here
    // ...

    // Let's assume another validation check fails after allocation
    if (header->id < 0) {
        printf("Error: Invalid packet ID.\n");
        // What's wrong with this return?
        return -1; 
    }
    
    // ...
    // Main processing logic using payload_buffer
    // ...

    printf("Packet %d processed successfully.\n", header->id);
    free(payload_buffer);
    return 0; // Success
}
  1. Identify the memory leak in the process_packet function.
  2. Refactor the function to eliminate the memory leak, ensuring payload_buffer is always freed if it was successfully allocated, regardless of which error occurs.
Show answer
  1. The Memory Leak: The leak occurs in the validation check for header->id. If header->id is less than 0, the function prints an error and returns -1 immediately. However, payload_buffer was already successfully allocated via malloc. By returning here, the function skips the free(payload_buffer) call at the end, orphaning the memory block. If this function is called repeatedly with invalid packet IDs, it will cause a significant memory leak.

  2. Refactored Implementation:

    A robust way to fix this is to ensure all exit paths after the allocation go through the free() call. Using a single return point is a clean way to achieve this.

    #include <stdlib.h>
    #include <stdio.h>
    
    #define MAX_PAYLOAD_SIZE 256
    
    typedef struct {
        int id;
        int payload_len;
    } PacketHeader;
    
    int process_packet(PacketHeader* header) {
        int return_code = 0; // Assume success initially
        char* payload_buffer = NULL; // Initialize to NULL
    
        if (header == NULL) {
            printf("Error: Null header.\n");
            return -1; 
        }
    
        if (header->payload_len <= 0 || header->payload_len > MAX_PAYLOAD_SIZE) {
            printf("Error: Invalid payload length.\n");
            return -1;
        }
    
        payload_buffer = (char*)malloc(header->payload_len);
        if (payload_buffer == NULL) {
            printf("Error: Payload buffer allocation failed.\n");
            return -1;
        }
        
        // Let's assume another validation check fails after allocation
        if (header->id < 0) {
            printf("Error: Invalid packet ID.\n");
            return_code = -1; // Set error code
            goto cleanup;      // Jump to cleanup block
        }
        
        // ... Main processing logic using payload_buffer ...
        printf("Packet %d processed successfully.\n", header->id);
    
    cleanup:
        free(payload_buffer); // Safe to call even if payload_buffer is NULL
        return return_code;
    }
    

    In this corrected version, any error that occurs after the allocation sets an error code and jumps to the cleanup label. This ensures free(payload_buffer) is always executed before the function returns, thus preventing the leak. The goto pattern, while often discouraged for general flow control, is highly effective for centralized resource cleanup in C.


Conclusion

Today you've explored the other half of the dynamic memory contract: deallocation. You now understand that failing to free() memory leads to leaks, which are especially dangerous in the long-running, safety-critical systems you work with.

Key Takeaways:

  • Every malloc, calloc, or realloc must eventually be paired with a call to free().
  • A memory leak is heap-allocated memory that the program can no longer access, and therefore cannot release.
  • Leaks are caused by either forgetting to call free() or losing the only pointer to the allocated block.
  • In long-running systems, even small leaks can accumulate, leading to performance degradation and system crashes.
  • free(ptr) deallocates the memory but does not change ptr, leaving it as a "dangling pointer."
  • Robust functions must be designed to free allocated resources on all possible execution paths, including error conditions.

Preview of the Next Lesson:
We've set the stage perfectly for our next topic. We briefly mentioned "dangling pointers" and the danger of using them. In the next lesson, we will "Identify and prevent common memory errors such as double-free and use-after-free." This will complete your understanding of the full lifecycle of dynamic memory and its most common pitfalls.