Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
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.
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.
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 into | Why it is safe | Watch out for |
|---|---|---|
| The caller’s own data (an array passed in) | The caller’s object outlives the call | Result is only valid while that array exists |
| Static or global storage | Lives for the whole program | Shared between calls and not reentrant |
| A caller-supplied buffer | Caller controls the lifetime and size | Must pass the size and check it |
Heap memory from malloc | Lives until freed | Caller 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 callingfree. 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
constdata can be placed in read-only memory by the linker.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Returning &local or a local array | Garbage values, random crashes | Return a pointer to caller data, static data, or use a buffer parameter |
Not checking for NULL on return | Hard fault when dereferencing | Test every returned pointer that can fail |
Writing through a returned const char * | Compile error, or crash on literals | Copy the string into your own buffer first |
| Using a pointer after the array it came from was changed or freed | Stale data | Keep the owner alive longer than the pointer |
| Returning a pointer to a static buffer from an ISR-shared function | Corrupted results | Use 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
bufandsize, return status or the same pointer. - Return
constpointers for data callers must not modify. - Return
NULLto 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
NULLon 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