Function Pointers
What Is a Function Pointer?
A function pointer holds the address of a function. You can call the function through the pointer, pass it as an argument, or store it in a data structure.
#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 (*op)(int, int); /* declare a function pointer */
op = add;
printf("add: %d\n", op(3, 2));
op = subtract;
printf("subtract: %d\n", op(3, 2));
return 0;
}
add: 5
subtract: 1
The declaration int (*op)(int, int) means: op is a pointer to a function that takes two int parameters and returns an int. The parentheses around *op are required; without them, int *op(int, int) declares a function returning int*.
Using typedef for Readability
Function pointer declarations are hard to read. A typedef gives them a clean name:
typedef int (*BinaryOp)(int, int);
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
void apply_and_print(BinaryOp op, int a, int b) {
printf("Result: %d\n", op(a, b));
}
int main(void) {
apply_and_print(add, 5, 3);
apply_and_print(multiply, 5, 3);
return 0;
}
Result: 8
Result: 15
With the typedef, BinaryOp replaces the unwieldy int (*)(int, int) everywhere.
Callback Patterns
A callback is a function pointer passed to another function, which calls it at the right time. This is the most common use of function pointers.
#include <stdio.h>
#include <stdlib.h>
typedef void (*EventHandler)(const char *event, void *user_data);
struct Button {
const char *label;
EventHandler on_click;
void *user_data;
};
void button_click(struct Button *btn) {
if (btn->on_click) {
btn->on_click("click", btn->user_data);
}
}
void my_handler(const char *event, void *user_data) {
const char *name = (const char *)user_data;
printf("Button '%s' received event: %s\n", name, event);
}
int main(void) {
struct Button btn = {
.label = "Submit",
.on_click = my_handler,
.user_data = "Submit",
};
button_click(&btn);
return 0;
}
Button 'Submit' received event: click
The void *user_data parameter lets the callback access context without global variables. This pattern appears in GUI frameworks, event loops, and asynchronous I/O libraries.
Function Pointer Arrays & Dispatch Tables
An array of function pointers creates a dispatch table, mapping an index to a function:
#include <stdio.h>
typedef double (*MathFunc)(double, double);
double op_add(double a, double b) { return a + b; }
double op_sub(double a, double b) { return a - b; }
double op_mul(double a, double b) { return a * b; }
double op_div(double a, double b) { return b != 0 ? a / b : 0; }
int main(void) {
MathFunc dispatch[] = {op_add, op_sub, op_mul, op_div};
const char *names[] = {"+", "-", "*", "/"};
double a = 10.0, b = 3.0;
for (int i = 0; i < 4; i++) {
printf("%.1f %s %.1f = %.2f\n", a, names[i], b, dispatch[i](a, b));
}
return 0;
}
10.0 + 3.0 = 13.00
10.0 - 3.0 = 7.00
10.0 * 3.0 = 30.00
10.0 / 3.0 = 3.33
Dispatch tables replace long switch statements and make it easy to add new operations.
qsort: The Standard Library Callback
The qsort function sorts an array using a comparator function pointer:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int compare_ints_ascending(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
return (ia > ib) - (ia < ib);
}
int compare_strings(const void *a, const void *b) {
const char *sa = *(const char **)a;
const char *sb = *(const char **)b;
return strcmp(sa, sb);
}
int main(void) {
int nums[] = {42, 7, 13, 99, 1};
int n = sizeof(nums) / sizeof(nums[0]);
qsort(nums, n, sizeof(int), compare_ints_ascending);
printf("Sorted ints: ");
for (int i = 0; i < n; i++) printf("%d ", nums[i]);
printf("\n");
const char *words[] = {"banana", "apple", "cherry", "date"};
int w = sizeof(words) / sizeof(words[0]);
qsort(words, w, sizeof(const char *), compare_strings);
printf("Sorted strings: ");
for (int i = 0; i < w; i++) printf("%s ", words[i]);
printf("\n");
return 0;
}
Sorted ints: 1 7 13 42 99
Sorted strings: apple banana cherry date
The comparator returns a negative value if a < b, zero if a == b, and a positive value if a > b. Never return a - b for integers because it can overflow.
The Strategy Pattern in C
The strategy pattern selects an algorithm at runtime. In C, this is a struct containing function pointers:
#include <stdio.h>
#include <string.h>
struct Logger {
void (*log)(const char *message, void *ctx);
void *ctx;
};
void log_to_stdout(const char *message, void *ctx) {
(void)ctx;
printf("[LOG] %s\n", message);
}
void log_to_file(const char *message, void *ctx) {
FILE *f = (FILE *)ctx;
fprintf(f, "[LOG] %s\n", message);
}
void do_work(struct Logger *logger) {
logger->log("Starting work", logger->ctx);
/* ... actual work ... */
logger->log("Work complete", logger->ctx);
}
int main(void) {
struct Logger console_logger = {
.log = log_to_stdout,
.ctx = NULL,
};
do_work(&console_logger);
FILE *f = fopen("app.log", "w");
if (f) {
struct Logger file_logger = {
.log = log_to_file,
.ctx = f,
};
do_work(&file_logger);
fclose(f);
}
return 0;
}
[LOG] Starting work
[LOG] Work complete
This is how C achieves polymorphism: a struct of function pointers replaces a vtable.
Signal Handlers
The signal function registers a callback for operating system signals:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
volatile sig_atomic_t running = 1;
void handle_sigint(int sig) {
(void)sig;
running = 0;
}
int main(void) {
signal(SIGINT, handle_sigint);
printf("Press Ctrl+C to stop...\n");
while (running) {
/* main loop */
}
printf("\nCaught SIGINT, shutting down.\n");
return 0;
}
The signal function takes a function pointer void (*)(int) as its second argument. Signal handlers must be async-signal-safe, meaning they can only call a limited set of functions and should only modify volatile sig_atomic_t variables.
Virtual Dispatch at the C Level
Object-oriented virtual dispatch is just a struct of function pointers. This is how C implements polymorphism, and it is how C++ vtables work under the hood.
#include <stdio.h>
#include <stdlib.h>
struct Shape;
struct ShapeVtable {
double (*area)(const struct Shape *self);
void (*describe)(const struct Shape *self);
void (*destroy)(struct Shape *self);
};
struct Shape {
const struct ShapeVtable *vtable;
};
/* Circle */
struct Circle {
struct Shape base;
double radius;
};
static double circle_area(const struct Shape *self) {
const struct Circle *c = (const struct Circle *)self;
return 3.14159265 * c->radius * c->radius;
}
static void circle_describe(const struct Shape *self) {
const struct Circle *c = (const struct Circle *)self;
printf("Circle with radius %.2f\n", c->radius);
}
static void circle_destroy(struct Shape *self) {
free(self);
}
static const struct ShapeVtable circle_vtable = {
.area = circle_area,
.describe = circle_describe,
.destroy = circle_destroy,
};
struct Shape *circle_create(double radius) {
struct Circle *c = malloc(sizeof(*c));
if (!c) return NULL;
c->base.vtable = &circle_vtable;
c->radius = radius;
return &c->base;
}
/* Rectangle */
struct Rect {
struct Shape base;
double width;
double height;
};
static double rect_area(const struct Shape *self) {
const struct Rect *r = (const struct Rect *)self;
return r->width * r->height;
}
static void rect_describe(const struct Shape *self) {
const struct Rect *r = (const struct Rect *)self;
printf("Rectangle %.2f x %.2f\n", r->width, r->height);
}
static const struct ShapeVtable rect_vtable = {
.area = rect_area,
.describe = rect_describe,
.destroy = circle_destroy, /* same free logic */
};
struct Shape *rect_create(double w, double h) {
struct Rect *r = malloc(sizeof(*r));
if (!r) return NULL;
r->base.vtable = &rect_vtable;
r->width = w;
r->height = h;
return &r->base;
}
int main(void) {
struct Shape *shapes[] = {
circle_create(5.0),
rect_create(4.0, 6.0),
circle_create(1.5),
};
for (int i = 0; i < 3; i++) {
shapes[i]->vtable->describe(shapes[i]);
printf("Area: %.2f\n\n", shapes[i]->vtable->area(shapes[i]));
}
for (int i = 0; i < 3; i++) {
shapes[i]->vtable->destroy(shapes[i]);
}
return 0;
}
Circle with radius 5.00
Area: 78.54
Rectangle 4.00 x 6.00
Area: 24.00
Circle with radius 1.50
Area: 7.07
The base struct contains a pointer to a vtable. Each concrete type provides its own vtable. Calling shape->vtable->area(shape) dispatches to the correct implementation.
Common Pitfalls
- Forgetting the parentheses.
int *fn(int)is a function returningint*.int (*fn)(int)is a pointer to a function returningint. The parentheses are critical. - Calling through a NULL function pointer. Always check that the pointer is not NULL before calling, especially for optional callbacks.
- Mismatched signatures. If the function pointer type expects
int (*)(int, int)and you assign a function with signatureint func(int), the behavior is undefined. - Casting function pointers. Casting between incompatible function pointer types is undefined behavior. Always match the expected signature exactly.
- Signal handler safety. Signal handlers run asynchronously. Calling
printf,malloc, or most standard library functions from a signal handler is undefined behavior. Only modifyvolatile sig_atomic_tvariables and call async-signal-safe functions.
Key Takeaways
- A function pointer stores the address of a function and allows calling it indirectly.
- Use
typedefto make function pointer types readable. - Callbacks decouple the caller from the implementation. The
void *user_datapattern provides context without globals. - Dispatch tables (arrays of function pointers) replace
switchstatements and scale easily. qsortis the textbook example of callbacks in the C standard library.- Structs of function pointers implement polymorphism and virtual dispatch, the same mechanism C++ uses under the hood.