Fundamentals 13 min read

Mastering C Function Pointers: From Basics to Embedded Applications

This tutorial explains what function pointers are, how to declare and use them in C, and demonstrates practical embedded‑system patterns such as callbacks, state machines, command dispatch, and advanced techniques like pointer arrays and struct members, while highlighting common pitfalls.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Mastering C Function Pointers: From Basics to Embedded Applications

What is a function pointer

In C, a function resides at a fixed address in the code segment. The function name evaluates to that address, so a function pointer is a variable that stores the entry address of a function.

Declaration syntax

The generic form is:

return_type (*pointer_name)(parameter_type_list);

Examples: int (*func_ptr)(int, int); // pointer to a function returning int and taking two int arguments void (*callback)(float); // pointer to a function returning void and taking a

float
char *(*get_string)(void);

// pointer to a function returning char * and taking no parameters

Parentheses around *pointer_name are mandatory; without them the declaration would describe a function returning a pointer.

Basic usage

Simple example

#include <stdio.h>

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main(void) {
    int (*operation)(int, int);
    operation = add;               // or operation = &add;
    int result1 = operation(10, 5); // equivalent to (*operation)(10, 5)
    printf("10 + 5 = %d
", result1);

    operation = subtract;
    int result2 = operation(10, 5);
    printf("10 - 5 = %d
", result2);
    return 0;
}

The two assignment forms are equivalent, and calling via operation(...) or (*operation)(...) yields the same result.

Using typedef to simplify

typedef int (*MathOperation)(int, int);
MathOperation op1 = add;
MathOperation op2 = subtract;
MathOperation operations[2] = { add, subtract };

This improves readability, especially when function pointers are passed around frequently.

Typical embedded applications

Callback mechanism

Many HAL libraries (e.g., STM32) invoke user‑defined callbacks through stored pointers:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM2) {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
    }
}

/* Inside the HAL driver */
if (htim->PeriodElapsedCallback != NULL) {
    htim->PeriodElapsedCallback(htim);
}

This decouples driver code from application logic.

State‑machine implementation

typedef void (*StateHandler)(void);

void state_idle(void)    { printf("System in IDLE state
"); }
void state_running(void) { printf("System in RUNNING state
"); }
void state_error(void)   { printf("System in ERROR state
"); }

typedef struct { StateHandler current_state; } StateMachine;
StateMachine sm;

void state_machine_init(void) { sm.current_state = state_idle; }
void state_machine_run(void)  { if (sm.current_state) sm.current_state(); }
void change_state(StateHandler new_state) { sm.current_state = new_state; }

The pattern is common in motor control, protocol handling, etc.

Command‑dispatch system

typedef void (*CommandHandler)(uint8_t *data, uint16_t len);

void cmd_read_sensor(uint8_t *data, uint16_t len)   { printf("Reading sensor data...
"); }
void cmd_write_config(uint8_t *data, uint16_t len) { printf("Writing configuration...
"); }
void cmd_reset_system(uint8_t *data, uint16_t len){ printf("Resetting system...
"); }

typedef struct { uint8_t cmd_id; CommandHandler handler; } CommandEntry;
CommandEntry command_table[] = {
    {0x01, cmd_read_sensor},
    {0x02, cmd_write_config},
    {0x03, cmd_reset_system}
};

void dispatch_command(uint8_t cmd_id, uint8_t *data, uint16_t len) {
    for (size_t i = 0; i < sizeof(command_table)/sizeof(CommandEntry); ++i) {
        if (command_table[i].cmd_id == cmd_id && command_table[i].handler) {
            command_table[i].handler(data, len);
            return;
        }
    }
    printf("Unknown command: 0x%02X
", cmd_id);
}

Adding a new command only requires extending the table, satisfying the open‑closed principle.

Advanced techniques

Function‑pointer arrays (menu example)

typedef void (*MenuFunction)(void);
void menu_item1(void){ printf("Executing menu item 1
"); }
void menu_item2(void){ printf("Executing menu item 2
"); }
void menu_item3(void){ printf("Executing menu item 3
"); }

MenuFunction menu_functions[] = { menu_item1, menu_item2, menu_item3 };

void execute_menu(int choice) {
    int menu_size = sizeof(menu_functions)/sizeof(MenuFunction);
    if (choice >= 0 && choice < menu_size) {
        menu_functions[choice]();
    } else {
        printf("Invalid menu choice
");
    }
}

Functions returning function pointers

typedef int (*Operation)(int, int);

Operation get_operation(char op) {
    switch (op) {
        case '+': return add;
        case '-': return subtract;
        default:  return NULL;
    }
}

Operation op = get_operation('+');
if (op) {
    int result = op(10, 5);
    printf("Result: %d
", result);
}

Function pointers as struct members (device driver example)

typedef struct {
    int  id;
    char name[32];
    void (*init)(void);
    void (*process)(uint8_t *data);
    void (*deinit)(void);
} Device;

void sensor_init(void)   { printf("Sensor initialized
"); }
void sensor_process(uint8_t *data) { printf("Processing sensor data
"); }
void sensor_deinit(void) { printf("Sensor deinitialized
"); }

Device sensor = { .id = 1, .name = "Temperature Sensor", .init = sensor_init, .process = sensor_process, .deinit = sensor_deinit };

sensor.init();
sensor.process(NULL);
sensor.deinit();

This mirrors object‑oriented design and is heavily used in Linux kernel drivers.

Pitfalls

Type safety

The signature of a function pointer must match the target function exactly; mismatched types lead to undefined behaviour.

// Dangerous: mismatched parameter types
float (*wrong_ptr)(float, float) = add; // undefined behaviour

// Correct
int (*correct_ptr)(int, int) = add;

Null‑pointer checks

void (*callback)(void) = NULL;
if (callback) {
    callback();
}

Initialization

Declare pointers with an explicit initializer (often NULL) to avoid wild pointers.

void (*handler)(void) = NULL; // safe default
// or
void default_handler(void) { /* … */ }
void (*handler)(void) = default_handler;

Conclusion

Function pointers enable dynamic dispatch, callbacks, and clean separation between framework and application code in embedded systems. They should be used when dynamic selection, decoupling, or extensibility is required, and avoided in simple scenarios where a direct function call is clearer.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

state machineCembeddedCallbacksfunction pointerstypedefcommand dispatch
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.