Skip to main content

Welcome to a Grasp lesson

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

Lesson 16

Returning Pointers from Functions: Pitfalls and Best Practices

Recommended resources

Hello! Welcome back to our third lesson in the "Pointers and Functions" module.

In our last session, we clarified the crucial distinction between pass-by-value and the simulation of pass-by-reference using pointers. We established that C is always pass-by-value, and when we pass a pointer, we're simply passing a copy of a memory address, giving the function the ability to modify the caller's data.

Today, we'll investigate the reverse scenario: returning a pointer from a function. This capability is essential for creating data structures whose lifetime must extend beyond the function that creates them. However, it also opens the door to one of C's most notorious and subtle bugs.

Your goal for this lesson is to learn how to return pointers from functions and identify associated pitfalls, including returning pointers to local variables. A deep understanding of variable lifetimes and the memory model, which we've been building upon, is paramount here.

Returning a Pointer: A Double-Edged Sword

Syntactically, returning a pointer is straightforward. You simply declare the function's return type to be a pointer to a specific data type, for example, int* my_function(). The real question is not how to do it, but what the returned pointer should point to.

Let's watch a video that perfectly illustrates how an attempt to return a pointer can seem to work at first, only to fail in unexpected ways.

Pointers as function returns in C/C++

The video 'Pointers as function returns in C/C++' by mycodeschool provides a narrative that starts with a seemingly working example and then reveals a critical logical error.

Please watch from the beginning until 07:25. Pay close attention to the code modification where the Add function is changed to return &c. Notice that it initially works, but then breaks after an unrelated function call is added.

As the video demonstrates, the program's output becomes corrupted after the PrintHelloWorld() function is called. Why does an unrelated function call break our Add function's logic? The answer lies in how the stack operates.


The Pitfall: Pointers to a Ghost

The variable c inside the Add function is a local variable. It exists on the stack and is only valid for the duration of the Add function's execution. When Add returns, its stack frame is marked as deallocated and available for reuse.

C Program Memory Layout
As we've seen, local variables reside in the Stack segment. When a function returns, its stack frame is popped, and that memory region is immediately considered free for the next function call.

The pointer returned by Add points to a memory location that is no longer reserved. While the value 6 might linger there for a moment, the very next function call (PrintHelloWorld) reclaims that stack space, overwriting the old data. The pointer you hold in main is now pointing to garbage.

Let's solidify this with a detailed explanation.

Pointers as function returns in C/C++

The next segment of the same video uses a clear stack diagram to visualize exactly why the memory location becomes invalid.

Now watch from 07:25 to 12:45. Focus on the visualization of the stack frames for main, Add, and PrintHelloWorld, and how the memory for Add's local variable c is overwritten.

This leads us to the single most important rule of this lesson:

Never return the address of a non-static local variable.

Pitfall: Returning Pointers to Local Variables
This slide states the rule unequivocally. The pointer returned from the function becomes invalid the moment the function exits, leading to undefined behavior.

This error creates what is formally known as a dangling pointer—a pointer that points to a memory location that has been deallocated or is no longer valid.

C_82 What is Dangling pointer in C | C Language Tutorials

This video from Jenny's Lectures CS IT introduces the term 'dangling pointer' and explains how returning a pointer to a local variable is a classic way to create one.

Watch from 12:33 to 15:39. This will formally connect the problem we've just seen with the correct terminology and also hint at a solution using the static keyword, which we'll discuss next.


Safe and Correct Ways to Return Pointers

If returning pointers to local variables is forbidden, how can functions create and return data? The key is to return a pointer to memory that will outlive the function call. There are two primary ways to achieve this.

1. Returning a Pointer to Dynamically Allocated Memory (Heap)

This is the most common and flexible method. Instead of creating a variable on the stack, you can explicitly request a block of memory from the heap using malloc(). Heap memory is not tied to any function's scope; it persists until you explicitly release it with free().

The Stack, The Heap, and Dynamic Memory Allocation

The article 'The Stack, The Heap, and Dynamic Memory Allocation' from Cornell University provides an excellent textual explanation of this concept.

Read the sections 'Limitations of the Stack' and 'The Heap'. The text clearly articulates the problem and then presents the correct solution using malloc, showing a complete before-and-after code example.

When a function allocates memory on the heap and returns a pointer to it, it effectively transfers ownership of that memory to the caller. The caller is now responsible for calling free() on that pointer when the data is no longer needed. This is a fundamental pattern in C and is critical for avoiding memory leaks.

Your experience with AUTOSAR middleware provides a great parallel. When a component in the communication stack (e.g., a PduR module) needs to provide data to an upper layer (e.g., a software component), it doesn't return a pointer to a local buffer. Instead, it might use a pre-allocated buffer from a memory pool (conceptually similar to the heap) and pass a reference to that buffer, ensuring the data remains valid after the function call completes.

2. Returning a Pointer to a static Local Variable

As hinted in the Jenny's Lectures video, another option is to declare the local variable as static. A static variable is not stored on the stack. Instead, it resides in the data segment of memory and its lifetime persists for the entire duration of the program.

int* get_counter() {
    static int counter = 0; // Stored in the data segment, not the stack.
    counter++;
    return &counter; // Safe: 'counter' lives for the whole program.
}

int main() {
    int* ptr1 = get_counter(); // *ptr1 is 1
    printf("Value: %d\n", *ptr1);
    
    int* ptr2 = get_counter(); // *ptr2 is 2. *ptr1 is also now 2!
    printf("Value: %d\n", *ptr2);

    return 0;
}

Caution: This approach has a significant drawback. There is only one instance of the static variable. Every call to the function returns a pointer to the exact same memory location. If a subsequent call modifies the value, all previously returned pointers will now "see" the new value, which can lead to very confusing bugs. This method is not re-entrant and is unsuitable for multi-threaded environments without synchronization. It's best used for simple, read-only data or internal counters where this side effect is understood and intended.

Test your understanding!

A junior engineer on your team writes a function to create and initialize a structure representing a simplified CAN message. Their code crashes.

#include <stdio.h>
#include <string.h>

typedef struct {
    unsigned int id;
    unsigned char dlc; // Data Length Code
    unsigned char data[8];
} CanMessage;

// BUGGY FUNCTION
CanMessage* create_can_message(unsigned int id, const char* payload) {
    CanMessage msg; // Local variable on the stack
    msg.id = id;
    msg.dlc = strlen(payload);
    memcpy(msg.data, payload, msg.dlc);
    
    return &msg; // Returns pointer to a variable that will be destroyed
}

int main() {
    CanMessage* my_msg = create_can_message(0x1A, "TEST");
    
    // CRASH! The memory my_msg points to has been overwritten.
    printf("Message ID: 0x%X\n", my_msg->id); 
    
    return 0;
}

How would you correct the create_can_message function using dynamic memory allocation so that it safely returns a valid CanMessage pointer? Don't forget to handle the caller's new responsibility.

Show answer

You need to allocate the CanMessage structure on the heap using malloc. The caller must then free the memory.

#include <stdio.h>
#include <string.h>
#include <stdlib.h> // Needed for malloc and free

typedef struct {
    unsigned int id;
    unsigned char dlc;
    unsigned char data[8];
} CanMessage;

// CORRECTED FUNCTION
CanMessage* create_can_message(unsigned int id, const char* payload) {
    // 1. Allocate memory on the heap
    CanMessage* msg_ptr = (CanMessage*)malloc(sizeof(CanMessage));
    
    // 2. Always check if malloc succeeded
    if (msg_ptr == NULL) {
        return NULL; // Return NULL to indicate failure
    }
    
    // 3. Initialize the allocated memory
    msg_ptr->id = id;
    msg_ptr->dlc = strlen(payload) > 8 ? 8 : strlen(payload); // Basic safety check
    memcpy(msg_ptr->data, payload, msg_ptr->dlc);
    
    // 4. Return the pointer to heap memory
    return msg_ptr;
}

int main() {
    CanMessage* my_msg = create_can_message(0x1A, "TEST");
    
    if (my_msg != NULL) {
        printf("Message ID: 0x%X\n", my_msg->id);
        
        // The caller is now responsible for freeing the memory
        free(my_msg); 
    }
    
    return 0;
}

Conclusion

Today, we've navigated one of the most critical aspects of using pointers with functions. By understanding the lifecycle of variables on the stack versus the heap, you can now confidently use functions to create and return data without falling into the trap of dangling pointers.

Key Takeaways:

  • The Pitfall: Never return a pointer to a non-static local variable. Its memory is on the stack and becomes invalid after the function returns.
  • The Consequence: Doing so creates a dangling pointer, which leads to undefined behavior, a common source of crashes and security vulnerabilities.
  • The Solution: To return a pointer to newly created data, allocate it on the heap using malloc().
  • The Responsibility: Returning a pointer from malloc transfers memory ownership. The calling function is now responsible for releasing the memory with free() to prevent memory leaks.

In our next lesson, we will focus on another very common and powerful interaction between pointers and functions: passing arrays to functions efficiently using pointer parameters. This will build directly on today's lesson, especially as we consider passing arrays that were dynamically created and returned from other functions.