What are Functions Returning Pointers in C – Embedded C Training For Beginners

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Free C Programming Course · Chapter 8 Pointers

Functions Returning Pointers in C

A function returning pointer in C can hand back a handle to an array element, a string or a buffer, but one wrong choice produces a dangling pointer that crashes only on some days. Learn the safe patterns in this free C tutorial for embedded C programming.

4
Tested examples
15
Minutes to read
C11
Standard used

What You Will Learn

  • How to declare and read a function returning pointer in C
  • Why returning the address of a local variable is a bug, and how the compiler warns you
  • Four safe places a returned pointer can point to
  • How to return a pointer into the caller’s array and use it to edit the data
  • How to return strings and fill caller-supplied buffers safely
  • Why embedded teams often avoid returning heap memory

Prerequisites

Make sure you are comfortable with pointer arithmetic, call by value and call by reference in C from the earlier lectures of this free embedded C course. You should also know that local variables live on the stack and disappear when a function ends.

Declaring a Function That Returns a Pointer

Normally a function returns a number. It may just as well return an address. You write the star next to the function name, in front of it, as part of the return type:

double *sensor_ptr(int id);      /* returns a pointer to a double */
const char *name_of(int code);   /* returns a pointer to read-only characters */
uint8_t *get_slot(unsigned idx); /* returns a pointer to a byte */

Read each declaration from the name outward: sensor_ptr is a function taking an int and returning a pointer to double. The caller stores the result in a matching pointer variable and uses it exactly like any other pointer, including dereferencing it with * to read or write.

The syntax is easy. The hard part is the lifetime question every pointer-returning function must answer: will the object I am pointing at still exist after I return?

The Classic Bug: Returning a Local Address

Local variables live in the function’s stack frame. When the function returns, the frame is released and the memory is reused by whatever is called next. A pointer to that memory is a dangling pointer. Here is the mistake in its simplest form:

#include <stdio.h>

static int *make_counter_wrong(void)
{
    int count = 5;

    return &count;     /* WRONG: count dies when the function returns */
}

int main(void)
{
    int *p = make_counter_wrong();

    (void)p;           /* never dereference p - it is a dangling pointer */
    return 0;
}

Compile it with warnings enabled and GCC points straight at the problem:

gcc -std=c11 -Wall -Wextra -o dangling dangling.c

dangling.c: In function 'make_counter_wrong':
dangling.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr]
    7 |     return &count;     /* WRONG: count dies when the function returns */
      |            ^~~~~~

We deliberately did not run or dereference this program, because using the pointer is undefined behaviour. It might print 5 today, print garbage tomorrow, or corrupt another variable after you add a new function call. Bugs that change with unrelated edits are the most expensive kind.

Why the Returned Address Goes Bad
1
During the call
count = 5 lives in the stack frame of make_counter_wrong().
2
At return
The frame is released. The address of count is copied to the caller.
3
After return
The next function call reuses that stack space. The pointer now aims at unrelated data.

Warning: Never return the address of a non-static local variable, a local array or a function parameter. Treat the compiler warning -Wreturn-local-addr as an error in your build flags.

Four Safe Sources for a Returned Pointer

Points intoWhy it is safeWatch out for
The caller’s own data (an array passed in)The caller’s object outlives the callResult is only valid while that array exists
Static or global storageLives for the whole programShared between calls and not reentrant
A caller-supplied bufferCaller controls the lifetime and sizeMust pass the size and check it
Heap memory from mallocLives until freedCaller must free; often avoided in firmware

Example 1: Return a Pointer Into the Caller’s Array

A natural use of a pointer-returning function is to hand back the location of an element instead of its value. The caller can then read it, edit it, or compute its index. This function finds the largest element and returns its address, or NULL when there is nothing to search.

#include <stdio.h>
#include <stddef.h>

static int *find_largest(int *data, size_t count)
{
    int *best;

    if (data == NULL || count == 0) {
        return NULL;
    }

    best = data;
    for (size_t i = 1; i < count; i++) {
        if (data[i] > *best) {
            best = &data[i];
        }
    }
    return best;
}

int main(void)
{
    int scores[] = {41, 87, 63, 92, 58};
    size_t n = sizeof scores / sizeof scores[0];
    int *top = find_largest(scores, n);

    if (top != NULL) {
        printf("largest = %d at index %td\n", *top, top - scores);
        *top = 0;                       /* edits the original array */
    }

    for (size_t i = 0; i < n; i++) {
        printf("%d ", scores[i]);
    }
    printf("\n");
    return 0;
}
gcc -std=c11 -Wall -Wextra -o largest largest.c
./largest

largest = 92 at index 3
41 87 63 0 58

The returned pointer is safe because scores lives in main() and is still alive. Subtracting the array name from the returned pointer gives the element index, and the type ptrdiff_t printed with %td is the correct type for such a difference. Writing through *top changed the original array, something a returned value could never do.

Example 2: Returning Strings With Static Storage

String literals live for the entire program, so a function can safely return a pointer to one. This is how lookup helpers for error names, state names and day names are usually written.

#include <stdio.h>
#include <stddef.h>

static const char *day_name(unsigned int day)
{
    static const char *const names[] = {
        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
    };

    if (day > 6u) {
        return NULL;
    }
    return names[day];
}

int main(void)
{
    const char *a = day_name(2);
    const char *b = day_name(9);

    printf("day 2 -> %s\n", (a != NULL) ? a : "invalid");
    printf("day 9 -> %s\n", (b != NULL) ? b : "invalid");
    return 0;
}
day 2 -> Tue
day 9 -> invalid

Two habits matter here. The return type is const char *, so callers cannot modify the literal text, which is undefined behaviour. And the function returns NULL for an invalid index so the caller can handle the error. In firmware, such constant tables are stored in flash, which saves precious RAM.

Example 3: Filling a Caller-Supplied Buffer

When the function must produce text that does not exist in advance, let the caller provide the storage. The function then returns the same pointer for convenience, or NULL when the buffer is too small.

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

static char *format_hex(uint8_t value, char *buf, size_t size)
{
    if (buf == NULL || size < 5) {      /* "0x1F" + terminating '\0' */
        return NULL;
    }
    snprintf(buf, size, "0x%02X", (unsigned int)value);
    return buf;
}

int main(void)
{
    char big[8];
    char tiny[3];
    char *s;

    s = format_hex(0x1F, big, sizeof big);
    printf("formatted : %s\n", (s != NULL) ? s : "buffer too small");

    s = format_hex(0x1F, tiny, sizeof tiny);
    printf("formatted : %s\n", (s != NULL) ? s : "buffer too small");
    return 0;
}
formatted : 0x1F
formatted : buffer too small

This pattern is the safest general-purpose answer. The caller decides where the memory comes from, whether stack, static or a pool, and the function never has to guess how long the result must live. It also removes any need for malloc, which many embedded coding standards forbid.

Embedded C Notes on Returning Pointers

  • Static buffers are not reentrant. If a function returns a pointer to its own static char buf[], an interrupt handler or a second RTOS task calling it will overwrite the first caller’s result.
  • Heap returns need an owner. A function that returns malloced memory silently makes the caller responsible for calling free. On a long-running device with fragmentation concerns, prefer static pools or caller buffers.
  • Peripheral base addresses are legitimate pointer returns: a helper such as volatile uint32_t *uart_regs(int port) hands back the fixed address of a register block that exists forever.
  • Const-correctness saves flash. Tables returned as const data can be placed in read-only memory by the linker.

Common Mistakes and Troubleshooting

MistakeSymptomFix
Returning &local or a local arrayGarbage values, random crashesReturn a pointer to caller data, static data, or use a buffer parameter
Not checking for NULL on returnHard fault when dereferencingTest every returned pointer that can fail
Writing through a returned const char *Compile error, or crash on literalsCopy the string into your own buffer first
Using a pointer after the array it came from was changed or freedStale dataKeep the owner alive longer than the pointer
Returning a pointer to a static buffer from an ISR-shared functionCorrupted resultsUse caller buffers or locking

Best Practices

  • Document who owns the memory a returned pointer refers to, and for how long it stays valid.
  • Prefer the caller-buffer pattern: pass buf and size, return status or the same pointer.
  • Return const pointers for data callers must not modify.
  • Return NULL to signal “not found” or “invalid”, and make every caller check it.
  • Performance: returning a pointer costs a single machine word, so it is the cheapest way to give access to a large object. The price is lifetime management.
  • Security: dangling pointers and unchecked sizes are the root of many memory safety vulnerabilities. Enable -Wall -Wextra, and consider AddressSanitizer (-fsanitize=address) while testing on a PC.

Key Takeaways

  • A function returning a pointer is declared as type *name(parameters).
  • Never return the address of a local, non-static variable: it is destroyed on return.
  • Safe targets are the caller’s data, static storage, a caller-supplied buffer, or heap memory with clear ownership.
  • Always return NULL on failure and check it at the call site.
  • Embedded code favours caller buffers and const tables over heap allocation.

Conclusion

Pointer-returning functions are powerful because they let you point at data without copying it, but they ask you to think about lifetime every single time. Before you write return p;, ask where the pointed-to object lives and who will outlive whom. If you can answer that clearly, the code is safe. If you cannot, switch to a caller-supplied buffer. Master this habit now and you will avoid some of the most stubborn bugs in embedded C programming. The final lecture of this set explains how arrays are passed to functions, which ties together everything you have seen about pointers and parameters.

Frequently Asked Questions

Can a C function return a pointer to a local variable?

It can compile, but using the result is undefined behaviour because the variable no longer exists after the function returns. Use a static variable, the caller’s data or a caller-supplied buffer instead.

Is it safe to return a pointer to a static local variable?

Yes, the object lives for the whole program. The drawbacks are that every call shares the same storage and the function is not reentrant.

Why does the function return const char * for strings?

String literals are read-only. The const qualifier lets the compiler stop you from trying to modify them.

What does a NULL return mean?

By convention it means “no valid result”, such as nothing found or an invalid argument. Always check for it before dereferencing.

Should embedded code use malloc in functions that return pointers?

Often not. Dynamic allocation can fragment memory and fail at run time. Static pools and caller-supplied buffers have predictable behaviour.

How can I catch dangling pointers while testing?

Compile with -Wall -Wextra to catch obvious cases, and run test builds on your PC with AddressSanitizer enabled to detect use-after-return and out-of-bounds access.

Learn Embedded C the Free Way

EmbeddedPathashala’s free C programming course builds you from basics to professional pointer skills. Next: passing arrays to functions in C, with a full quiz and interview questions.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *