↓ Skip to main content

C Function Pointers Explained: Declaration, Usage, and Examples

C Function Pointers Explained with Examples

Function pointers are one of the most useful features in C, particularly when building callback mechanisms, event-driven applications, device drivers, and embedded software.

Unlike ordinary pointers, which store the addresses of data objects, function pointers hold references to functions. This allows a program to select and invoke different functions at runtime, pass behavior as an argument, and build flexible interfaces without relying on object-oriented language features.

If you work with embedded C, operating systems, or low-level application development, understanding function pointers is essential.

🧠 What Is a Function Pointer?
#

When a function is defined in C, it has an associated entry point that can be referenced through a function pointer.

A function name used in an expression is generally converted into a pointer to that function. This allows the program to invoke the function indirectly through a pointer.

For example, consider this function:

int GetData(int a, int b)
{
    return a + b;
}

You can declare a function pointer that references it:

int (*p)(int, int);

The declaration means:

  • p is a pointer to a function.
  • The function accepts two int arguments.
  • The function returns an int.

You can then assign the function to the pointer:

p = GetData;

And invoke it:

int result = p(10, 20);

The result is 30.

A function pointer provides an indirect way to execute a function without calling its name directly.

📝 Declaring a Function Pointer
#

Function pointer declarations can look confusing at first because C uses parentheses to distinguish pointers to functions from functions that return pointers.

Consider the following declarations:

int GetData(int a, int b);  // Function declaration

int (*p)(int a, int b);     // Function pointer declaration

The parentheses around *p are required because of C operator precedence.

Compare the following two declarations:

int (*p)(int, int);

This declares p as a pointer to a function returning int.

int *p(int, int);

This declares p as a function accepting two integers and returning a pointer to int.

These declarations describe completely different types.

Understanding Function Pointer Types
#

A function pointer must have a type compatible with the function it references.

For example:

int Add(int a, int b)
{
    return a + b;
}

int (*operation)(int, int);

operation = Add;

The assignment is valid because the function signature matches the pointer type.

A mismatch in return type or parameter types can produce compiler diagnostics and, if an incompatible function pointer is called, undefined behavior.

For larger projects, using typedef makes function pointer declarations easier to read:

typedef int (*operation_t)(int, int);

operation_t operation = Add;

This approach is particularly useful when function pointers appear in structures, callback interfaces, or arrays.

⚙️ Calling Functions Through Pointers
#

Let’s start with a simple example demonstrating direct and indirect function calls.

Example: Basic Function Pointer
#

#include <stdio.h>

void PrintWelcome(void)
{
    printf("Hello, welcome\n");
}

int main(void)
{
    // Direct function call
    PrintWelcome();

    // Declare and initialize a function pointer
    void (*p)(void) = PrintWelcome;

    // Indirect function call
    p();

    // Equivalent indirect call
    (*p)();

    return 0;
}

Output
#

Hello, welcome
Hello, welcome
Hello, welcome

All three calls execute the same function.

In C, these two expressions are equivalent:

p();
(*p)();

The first form is generally preferred because it is shorter and easier to read.

There is one important detail in the declaration:

void (*p)(void);

Using void inside the parameter list explicitly specifies that the function takes no arguments. An empty parameter list, such as void (*p)(), has different semantics in traditional C declarations because it does not specify the parameter types.

🔧 Why Use Function Pointers?
#

Function pointers are useful when the program needs to select behavior dynamically instead of hardcoding every function call.

Common applications include:

  • Runtime function selection.
  • Callback mechanisms.
  • Event-driven programming.
  • Interrupt and driver interfaces.
  • State machines.
  • Table-driven programming and dispatch tables.
  • Generic algorithms that accept custom operations.

For example, a device driver might expose a common interface while using different implementations for different hardware devices.

The application calls the same interface, but the underlying function depends on the selected device.

This is one reason function pointers are widely used in embedded software and operating system development.

💻 Example: Function Pointers with a Switch Statement
#

Consider a program that performs one of three mathematical operations based on user input:

  • 1: Calculate the maximum of two integers.
  • 2: Calculate the minimum of two integers.
  • 3: Calculate their sum.

A conventional implementation could place the calculation directly inside each switch branch.

Instead, we can use function pointers to separate operation selection from operation execution.

Complete C Implementation
#

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

// Three mathematical operations
int GetMax(int a, int b)
{
    return (a > b) ? a : b;
}

int GetMin(int a, int b)
{
    return (a < b) ? a : b;
}

int GetSum(int a, int b)
{
    return a + b;
}

// Function pointer handler
int DataHandler(int a, int b,
                int (*operation)(int, int))
{
    if (operation == NULL)
    {
        return -1;
    }

    return operation(a, b);
}

int main(void)
{
    int a = 10;
    int b = 20;
    int cmd;
    int ret;

    // Declare a function pointer
    int (*operation)(int, int) = NULL;

    printf("Enter 1 for max, 2 for min, 3 for sum: ");

    if (scanf("%d", &cmd) != 1)
    {
        printf("Invalid input\n");
        return EXIT_FAILURE;
    }

    // Select the function at runtime
    switch (cmd)
    {
        case 1:
            printf("Maximum: ");
            operation = GetMax;
            break;

        case 2:
            printf("Minimum: ");
            operation = GetMin;
            break;

        case 3:
            printf("Sum: ");
            operation = GetSum;
            break;

        default:
            printf("Invalid input\n");
            return EXIT_FAILURE;
    }

    // Execute the selected function
    ret = DataHandler(a, b, operation);

    printf("ret = %d\n", ret);

    return EXIT_SUCCESS;
}

Example Run
#

Run 1: Maximum

$ ./test

Enter 1 for max, 2 for min, 3 for sum: 1
Maximum: ret = 20

Run 2: Minimum

$ ./test

Enter 1 for max, 2 for min, 3 for sum: 2
Minimum: ret = 10

Run 3: Sum

$ ./test

Enter 1 for max, 2 for min, 3 for sum: 3
Sum: ret = 30

How the Implementation Works
#

The key declaration is:

int (*operation)(int, int) = NULL;

It defines a function pointer capable of referencing functions that accept two integers and return an integer.

The switch statement assigns the appropriate function:

operation = GetMax;

The selected function is then passed to DataHandler():

ret = DataHandler(a, b, operation);

Inside the handler, the function is invoked indirectly:

return operation(a, b);

This separates the decision-making logic from the actual operation.

Adding another operation only requires implementing another compatible function and extending the selection logic.

For larger applications, an array of function pointers can replace a long switch statement. This is commonly known as a dispatch table.

🔍 Key Rules and Common Pitfalls
#

Function pointers are straightforward once their declaration syntax and type requirements are understood. However, several details deserve attention.

1. Always Use Parentheses in Function Pointer Declarations
#

Correct:

int (*pfunk)(int, int);

Incorrect for declaring a function pointer:

int *pfunk(int, int);

The second declaration defines a function returning an integer pointer.

2. Match the Function Signature
#

The pointer type must be compatible with the target function.

For example:

int Add(int a, int b)
{
    return a + b;
}

int (*operation)(int, int) = Add;

This is valid.

A function with a different return type or incompatible parameter types cannot safely be called through this pointer.

3. Function Parameter Names Are Optional
#

Both declarations are valid:

int (*pfunk)(int, int);
int (*pfunk)(int data1, int data2);

Parameter names are useful for documentation, but they are not required when declaring a function pointer type.

4. Check Function Pointers Before Calling Them
#

A function pointer can be initialized to NULL when no function has been selected.

Before invoking it, verify that it references a valid function:

if (pfunk != NULL)
{
    pfunk(10, 20);
}

Calling through a null or otherwise invalid function pointer results in undefined behavior.

5. Function Pointers Can Be Passed as Parameters
#

One of their most useful features is the ability to pass a function as an argument to another function.

For example:

int Execute(int a, int b, int (*func)(int, int))
{
    if (func == NULL)
    {
        return -1;
    }

    return func(a, b);
}

This technique forms the foundation of callback-based programming.

🚀 Practical Applications in Embedded C
#

Function pointers are particularly valuable in embedded development because they provide a lightweight way to organize hardware-specific behavior.

Typical applications include:

  • Hardware abstraction layers (HAL).
  • Device driver operation tables.
  • Interrupt callback registration.
  • RTOS event handling.
  • Communication protocol handlers.
  • Finite state machine implementations.

For example, a driver interface can place function pointers inside a structure:

typedef struct
{
    int (*init)(void);
    int (*read)(unsigned char *buffer, int length);
    int (*write)(const unsigned char *buffer, int length);
} device_ops_t;

Different hardware implementations can provide their own functions while exposing the same interface to application code.

This design reduces hardware dependencies and makes driver implementations easier to replace and test.

For additional examples, see Advanced Application Scenarios of Function Pointers in C, which covers callbacks, sorting, dispatch tables, and runtime polymorphism.

📋 Function Pointer Quick Reference
#

Declaration Meaning
int (*p)(int, int); Pointer to a function returning int
void (*p)(void); Pointer to a function returning void with no parameters
p = GetMax; Assign a function to a pointer
p(10, 20); Invoke the function through the pointer
(*p)(10, 20); Equivalent indirect function call
typedef int (*func_t)(int, int); Define a reusable function pointer type

Understanding these declarations makes it much easier to read existing C code, especially driver interfaces, callback registrations, and function-pointer-based APIs.

Related