Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Reading Function Pointer Declarations
Decode any C function pointer declaration with a simple three-step method. Part of the free C programming course for embedded systems.
What You Will Learn in This Lecture
- Why function pointer declarations in C look harder than they are
- A three-step method that decodes any declaration, however nested
- The difference between a function that returns a pointer and a pointer to a function
- How to declare arrays of function pointers and call through them
- How
typedefturns an unreadable declaration into a one-word type - Where this appears in embedded C programming: callbacks, command tables, interrupt vectors
Prerequisites
You should be comfortable with basic pointer declaration, the address-of and dereference operators, and how functions return values. If you are following this free C programming course in order, the previous lectures in the Pointers chapter cover everything needed.
Why Function Pointer Declarations in C Look Confusing
Every function pointer declaration is built from only four ingredients: an identifier, a *, a pair of parentheses holding parameters, and a return type. The trouble is not the ingredients but the **precedence**. In a declarator, the function-call parentheses () and the array brackets [] bind tighter than *. So int *f(void) means “f is a function returning int *“, not “f is a pointer to a function”. To get the second meaning you must force the * to bind to the name first by wrapping them: int (*f)(void).
This single rule explains almost every confusing line you will meet in a free C tutorial or in real embedded firmware. Once you accept that the parentheses around *name are doing the job of “bind this to a pointer first”, the rest is mechanical.
A Three-Step Method to Read Any Declaration
- **Find the name.** Start at the identifier. Everything is read outward from it.
- **Go right, then left.** Read what sits immediately to the right of the name (
()means “function taking…”,[N]means “array of N…”). Only when the right side is exhausted or closed by a)do you move left to read*as “pointer to”. - **Parentheses first.** If the name is inside a pair of parentheses, finish the inside completely, then step outside and continue with the right side, then the left side.
Decoding int *(*getp)(void) step by step
Read in one breath: **getp is a pointer to a function that takes no arguments and returns a pointer to int.**
Five Declarations Decoded
The table below covers the shapes you meet most often. Change the names and parameter lists and the same reading method still works.
| Declaration | Plain-English reading |
|---|---|
int (*cb)(void); | cb is a pointer to a function with no arguments that returns int |
int (*cb_table[4])(void); | cb_table is an array of 4 pointers, each to a function with no arguments returning int |
int *(*getp)(void); | getp is a pointer to a function with no arguments that returns an int pointer |
float *(*mk)(int, float); | mk is a pointer to a function taking an int and a float and returning a float pointer |
float *(*mk_tab[4])(int, float); | mk_tab is an array of 4 pointers, each to a function taking int and float and returning a float pointer |
Where the array brackets sit
Tip: In cb_table[4] the brackets are inside the parentheses, right next to the name. That is why the array is of pointers. Move the brackets outside and the declaration stops being valid, because C does not allow an array of functions.
Pointer to Function vs Function Returning a Pointer
This is the mistake that costs beginners the most time. Compare two declarations that differ by one pair of parentheses.
| Declaration | What it is | Can you assign get_slot to it? |
|---|---|---|
int *p(void); | a **function** named p returning int * | No, it is a prototype, not a variable |
int (*p)(void); | a **pointer variable** that can hold the address of such a function | Yes: p = get_slot; |
Remember: a function name used in an expression already decays to its address, exactly as an array name decays to the address of its first element. So p = get_slot; and p = &get_slot; are equivalent, and p() and (*p)() call the same function.
Complete Example: Declaring and Calling Through Pointers
The program below declares one pointer for each shape from the table, plus an array of three pointers. The functions are small stand-ins for something you would see in embedded C programming: reading a sensor, fetching a configuration slot, and scaling a raw ADC count.
#include <stdio.h>
static int read_sensor(void) { return 42; }
static int *get_slot(void) { static int slot = 7; return &slot; }
static float *scale(int raw, float gain) { static float out; out = (float)raw * gain; return &out; }
int main(void)
{
int (*p_read)(void) = read_sensor;
int *(*p_slot)(void) = get_slot;
float *(*p_scale)(int, float) = scale;
int (*table[3])(void) = { read_sensor, read_sensor, read_sensor };
printf("read : %d\n", p_read());
printf("slot : %d\n", *p_slot());
printf("scaled : %.2f\n", *p_scale(10, 0.5f));
int sum = 0;
for (int i = 0; i < 3; i++)
sum += table[i]();
printf("table : %d\n", sum);
printf("sizeof(table) = %zu, sizeof(table[0]) = %zu\n", sizeof table, sizeof table[0]);
return 0;
}Build and run it. The -std=c17 flag works on any recent GCC or Clang, and the code also compiles under C23.
gcc -std=c17 -Wall -Wextra -o fp_decl fp_decl.c
./fp_declExpected output on a 64-bit Linux machine:
read : 42
slot : 7
scaled : 5.00
table : 126
sizeof(table) = 24, sizeof(table[0]) = 8Three things to notice. First, p_slot() returns an int *, so the result is dereferenced with * before printing. Second, *p_scale(10, 0.5f) parses as *(p_scale(10, 0.5f)): the call happens first, then the dereference. Third, sizeof(table) is 24 because three pointers of 8 bytes each sit in the array; on a 32-bit microcontroller it would be 12.
Warning: Both get_slot and scale return the address of a static object. Returning the address of an ordinary local variable would hand the caller a dangling pointer. The static keeps the storage alive after the function returns.
Making Declarations Readable with typedef
When a function pointer type appears in more than one place, name it once. The typedef below captures the whole shape, and the array declaration becomes trivial to read.
#include <stdio.h>
typedef int (*op_fn)(void);
static int one(void) { return 1; }
static int two(void) { return 2; }
int main(void)
{
op_fn ops[2] = { one, two };
printf("%d %d\n", ops[0](), ops[1]());
return 0;
}1 2The name op_fn now stands for “pointer to a function taking no arguments and returning int“. Most production embedded code, including vendor HAL libraries, uses exactly this style for callbacks.
Real-World Uses in Embedded C Programming
- **Interrupt vector tables:** an array of pointers to handler functions, indexed by interrupt number.
- **Command dispatchers:** a UART shell maps the typed command to a function pointer in a lookup table.
- **Driver ops tables:** a device exposes a struct of function pointers (
open,read,write) so upper layers stay hardware independent. - **State machines:** each state is a function, and the current state is a single function pointer.
Command dispatch table
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Missing parentheses: int *f(void) when a pointer was meant | Warning about initialization from incompatible type, or a “function redeclared” error | Write int (*f)(void) |
Calling a pointer that is still NULL | Segmentation fault at the call | Check if (cb != NULL) before calling |
| Wrong parameter list in the pointer type | Compiler warning about incompatible pointer type; undefined behaviour if ignored | Make the pointer type match the function signature exactly |
Writing fp = func() instead of fp = func | Assigns the return value, not the address | Drop the parentheses when taking the address |
Best Practices, Performance and Security
- Use a
typedefthe moment a function pointer type is used twice. - Prefer
consttables:static int (* const handlers[])(void)keeps the table in flash on a microcontroller and stops accidental overwrites. - An indirect call costs a load plus an indirect branch and cannot be inlined, so avoid it in the innermost loop of timing-critical code.
- Treat any writable function pointer as a security-sensitive object: an attacker who can overwrite it controls where execution goes. Keep tables
constand validate indexes before dispatch. - Always initialise function pointers, either to a real function or to
NULL, and check before calling.
Summary and Key Takeaways
()and[]bind tighter than*, so(*name)is needed to declare a pointer to a function.- Read a declaration from the name outward: right first, then left, parentheses first.
int *p(void)is a function;int (*p)(void)is a pointer to a function.name[N]inside the parentheses makes an array of function pointers.- A
typedefremoves nearly all the visual noise.
Conclusion
Function pointer declarations stop being intimidating once you stop reading them left to right and start reading them from the name outward. Practise the three-step method on every declaration you meet in header files and vendor libraries; after a week it becomes automatic. In the next lecture of this free embedded C course we move to a different pointer hazard: pointers that are used before they point anywhere valid.
Frequently Asked Questions
What is a function pointer in C?
A function pointer is a variable that stores the address of a function. You can call the function through it, pass it to other functions as a callback, or keep many of them in an array.
How do I read a complicated function pointer declaration?
Start at the identifier, read right (parameters or array size), then left (* means pointer to, the type on the far left is the return type), and always finish anything inside parentheses first.
What is the difference between int *f(void) and int (*f)(void)?
The first declares a function returning a pointer to int. The second declares a pointer variable that can point to a function returning int.
Do I need the ampersand when assigning a function to a pointer?
No. A function name already converts to its address, so fp = func; and fp = &func; mean the same thing.
Can I make an array of function pointers?
Yes, for example int (*table[4])(void);. An array of functions themselves is not allowed in C, only an array of pointers to them.
Why are function pointers common in embedded C programming?
They give you callbacks, interrupt vector tables, command dispatch and driver interfaces without needing an operating system or an object model.
Keep Learning Embedded C for Free
Continue the free C programming course with the next pointer lecture and test yourself on every topic.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments