C code can treat functions as values by taking their addresses, storing those addresses in variables, passing them into other functions, and calling them later. That behavior sits behind callback based APIs in many C libraries, from sorting helpers to event loops. Function pointers in C connect syntax for taking a function address with the call mechanics that let a pointer trigger that function, and they make it possible to pass custom behavior into generic utilities such as sort helpers, search helpers, or event dispatch tables that work with different data types.
Function Pointer Basics
Stored code and stored data meet through function pointers. Each function lives at some address in memory, and a function pointer variable holds that address so it can be passed around, stored, or replaced. In C, that pointer value interacts with the rules that treat function names as callable objects in expressions and with declaration syntax that tells the compiler a particular variable is a pointer to a function rather than a function that returns a pointer.
Function Addresses as Values
Every function that you define in C has a fully specified type. If a function takes two int parameters and returns an int, the type is “function taking int, int and returning int”. That type matters because any pointer variable that holds the address of the function has to use a compatible type, and the compiler checks that relationship.
In most expression contexts, a function name behaves as if it were the address of that function. You can think of the function identifier in an expression as being converted to a pointer value of the matching function pointer type. That value can then be assigned to variables, passed to other functions, or stored in arrays and structures. The pointer itself is just data, even though it refers to executable code.
Let’s look at a small example that makes the idea of treating a function as a value that can move between variables easier to see:
That small main function shows three important details in a single place. The declaration int (*op)(int, int) states that op is a pointer to a function with two int parameters and an int result, the assignments op = add and op = subtract treat the function names as addresses, and the calls op(3, 4) and op(7, 2) use the same syntax you would use with a direct function call. The call through a pointer could also be written as (*op)(3, 4) and the behavior would match.
Function pointers form their own category of pointer types. Pointers to int or void that refer to data in memory are not interchangeable with pointers to functions. The C standard does not guarantee that a function pointer can be safely converted to a data pointer type such as void * and back again, and different platforms handle these kinds of addresses in different ways. Portable C code keeps function pointers in variables whose types are declared as function pointers and avoids casting them through void * or integer types unless a specific platform document says that such casts are supported.
Equality comparison on function pointers is defined, which means two function pointer values can be checked with == or != to see whether they refer to the same function. Arithmetic on function pointers is not allowed, so expressions such as adding an integer to a function pointer are rejected by the compiler. That restriction matches the idea that there is no meaningful “next function” address in ordinary C source.
Many projects also treat a null function pointer as “no function for this slot”. That style is common in callback tables and optional hooks.
The logger variable begins as a null pointer, so the first conditional block does nothing. After assignment, the same variable holds the address of log_message, and the second conditional block calls that function through the pointer. That pattern treats the presence or absence of a function pointer as a basic configuration value that influences control flow.
Function pointer values follow the usual rules for lifetime and storage duration in C. If a function pointer variable is global, the stored address lasts for the entire run of the process. If the variable lives on the stack inside a function, such as a local op variable in main, then the pointer value itself only exists while that stack frame is active, even though the function that it refers to exists for the whole run.
Current C standards such as C11, C17, and C23 keep these rules, and typical compilers apply them in the same way on common desktop and server platforms.
Function Pointer Declaration Syntax
Reading and writing function pointer declarations trips up many people because the syntax has more punctuation than a plain data pointer. The general idea is that you declare a pointer to a function by placing an asterisk in front of the variable name, wrapping that part in parentheses, and then writing the parameter list and return type in the same form used for an ordinary function.
The common form looks like this:
The parentheses around *name change the binding of the * operator. Without the parentheses, the declaration would instead declare a function that returns a pointer, not a pointer to a function. Keeping those parentheses in place guides both the compiler and future readers.
Some small declarations give a sense of how the syntax adapts to different function types:
Those three lines introduce variables that can point at very different bodies of code. The first can hold the address of any function that takes one int and returns an int, such as an absolute value helper or a clamp. The second describes a pointer to a function that accepts two double values and returns a double. The third points at a function with no parameters and no return value, which suits short notification callbacks and similar helpers.
It’s common to see developers follow the rule that declarations mirror expressions. You can start at the identifier, step outward through parentheses and operators, and then read the base type. For int (*unary_op)(int) you first see unary_op, then note that it is a pointer * in parentheses, then read the parameter list (int) and return type int. That mental process matches how a call expression like unary_op(10) works at runtime.
Projects with larger codebases tend to introduce typedef names for function pointer types so that later declarations are easier to read and work with. Such a typedef lets the complex syntax appear one time and gives a short label that you can reuse:
The typedef int_binary_fn declares a type that stands for “pointer to function taking two int parameters and returning int”. The print_result function accepts an int_binary_fn along with a label and two integer values, then calls the function pointer to compute and print a result. That setup keeps the call site in main neat, while still recording the full function type in a single place.
There are also cases where function pointers appear inside other aggregates. Arrays of function pointers and structures that hold a function pointer field are both common in callback tables and dispatch logic:
The operations array carries two function pointers, and the indexed call expressions operations[0](x, y) and operations[1](x, y) make the behavior easy to see because a function pointer in an array slot behaves like any other function pointer variable when you call it. When that declaration syntax becomes familiar, function pointers can sit in arrays, structures, and other containers in the same way as data pointers, with the same rules about lifetime and scope that apply to regular variables.
Callbacks Through Function Pointers
Function pointers become callbacks when one function accepts a pointer to another function and later calls that pointer. Control flow moves through a library routine, then jumps into code supplied by the caller, then returns to the library again. That jump back into caller supplied code is what gives callbacks their character in C. The mechanics rely entirely on ordinary function pointers and call expressions rather than any special syntax.
Callback Concept
Callbacks in C revolve around parameters whose types are function pointers. A library routine declares a parameter with a function pointer type, and callers pass the address of a function that matches that type. During execution, the library routine uses that function pointer in a call expression, which transfers control to the caller’s function. Work that depends on project specific behavior sits in the callback, while common control flow stays in the library routine.
Take this for example:
The function run_twice has one parameter, fn, whose type is “pointer to function taking no parameters and returning void”. The call run_twice(greet) passes the address of greet, and inside run_twice the two calls fn() transfer control to greet. The run_twice routine never needs to know what greet prints or how long it runs, only that the type matches and that it can be called with no parameters.
Callbacks can also accept data along with the function pointer. Many C libraries include a context pointer or user data pointer that is passed back into the callback on every call. That pointer lets the callback read or update state without relying on global variables.
In this one, the visit_range function walks from start to end and calls a callback for each value. The callback add_to_sum receives both the current integer and a pointer to sum_data. Passing the same context pointer back on every call keeps the iteration logic separate from the accumulation logic while still letting the callback see the current partial sum.
In both examples the callback functions have addresses that the linker and loader set up, and the outer routines hold function pointers that can be swapped out or combined with other data. That combination is what makes callbacks practical in C without any extra language features.
Sorting With qsort Callbacks
The qsort function from <stdlib.h> is one of the best known callback based functions in the C standard library. It sorts an array of elements that all have the same size and uses a comparison callback to decide ordering between any two elements. The declaration in current headers looks like this:
The base pointer marks the first element in the array, nmemb gives the number of elements, and size tells qsort how many bytes to move when it swaps elements. The last parameter, compar, is a pointer to a function that can compare two elements. That comparison function receives two const void * values that point to elements inside the array and returns an int that signals how those elements relate to one another. By convention, a value less than zero says the first element should come before the second, zero says they are equal for sorting purposes, and a value greater than zero says the first should come after the second.
Sorting an array of integers with qsort gives a direct view of how the callback works in practice:
The compare_ints function matches the type that qsort expects. Each const void * parameter is cast back to const int *, then dereferenced to read the integer values. The chain of if and else if checks returns a negative value, zero, or a positive value based on how the two integers compare. During the sort, qsort calls compare_ints through the function pointer to ask which of two elements should come first, and it applies that decision many times as it rearranges the array.
Element types do not have to be plain numbers. A more interesting case uses structures and applies sorting rules that look at multiple fields.
The comparison compare_records sorts by score in descending order and breaks ties by increasing id. Converting the incoming const void * pointers back to const struct record * allows direct access to fields. During the run, qsort asks compare_records which record should go first for many different pairs of elements until the array matches those rules.
One important detail is that the comparison function has to be consistent. If it says that a comes before b, and b comes before c, then comparisons between a and c must not contradict that. Sorting algorithms assume that the comparison relation is transitive and consistent in that sense. Violating that expectation can lead to strange ordering or longer runtimes, even if the callback compiles and runs without crashes.
The qsort interface has stayed steady across C standard revisions, and the use of a function pointer for comparison is common in many C libraries that need customizable ordering.
Callback Idioms In Application Code
Callbacks show up in everyday C code outside of the standard library. Projects that handle events, walk data structures, or schedule work often rely on arrays or tables of function pointers, with each entry matching a particular event code or context.
An event dispatcher gives a compact example. Each event code is mapped to a handler function pointer, and a central dispatcher looks up the correct handler and calls it.
The handlers array binds event codes to handler functions. The dispatcher does not need to know what each handler prints or how it manages state. It only checks event codes, finds the matching slot, and calls the function pointer in that slot. Missing entries fall back to a default message. Adding support for new events means adding more entries, not editing the dispatcher loop.
Traversal callbacks bring the same idea into data structure walks. Library code can walk a tree or list while user supplied callbacks process nodes.
Here, the inorder_visit routine handles traversal logic, and the callback print_node handles printing. A caller that wants to count nodes, track statistics, or collect values into an array can pass a different visitor function and context pointer without changing traversal code. Each visitor is just another function pointer that fits the node_visitor type.
Systems that schedule work on threads or timers use the same idea. Thread pools can accept a function pointer plus a void * pointer and enqueue that pair as a single unit of work. Timer facilities can accept a callback that runs after a delay along with an opaque pointer to user data. The scheduler manages timing and threading, while callbacks carry out project specific work when their turn comes.
In all of these idioms, function pointers act as small, flexible hooks. Code that owns the main control flow keeps arrays or lists of function pointers as configuration, and those pointers connect that flow to specific behavior written elsewhere in the codebase.
Conclusion
Function pointers let C treat code as data by taking function addresses, storing them in variables, and calling through those variables whenever control flow needs a callback. The same mechanics appear in function types, in the syntax that marks a variable as a pointer to a function, and in call expressions that use a pointer instead of a fixed function name. Those parts explain how callbacks such as qsort comparators, event handlers, and visitor functions plug custom behavior into generic control flow while still going through ordinary C types and function calls.







![#include <stdio.h> typedef int (*operation_fn)(int, int); int add_ints(int a, int b) { return a + b; } int multiply_ints(int a, int b) { return a * b; } int main(void) { operation_fn operations[2]; operations[0] = add_ints; operations[1] = multiply_ints; int x = 6; int y = 7; printf("add_ints: %d\n", operations[0](x, y)); printf("multiply_ints: %d\n", operations[1](x, y)); return 0; } #include <stdio.h> typedef int (*operation_fn)(int, int); int add_ints(int a, int b) { return a + b; } int multiply_ints(int a, int b) { return a * b; } int main(void) { operation_fn operations[2]; operations[0] = add_ints; operations[1] = multiply_ints; int x = 6; int y = 7; printf("add_ints: %d\n", operations[0](x, y)); printf("multiply_ints: %d\n", operations[1](x, y)); return 0; }](https://substackcdn.com/image/fetch/$s_!0px9!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa20bca92-fc18-43bc-b958-071925150b73_1773x878.png)



![#include <stdio.h> #include <stdlib.h> int compare_ints(const void *a, const void *b) { const int *ia = (const int *)a; const int *ib = (const int *)b; if (*ia < *ib) { return -1; } else if (*ia > *ib) { return 1; } else { return 0; } } int main(void) { int values[] = {7, 19, 3, 100, 5}; size_t count = sizeof values / sizeof values[0]; qsort(values, count, sizeof values[0], compare_ints); for (size_t i = 0; i < count; i++) { printf("%d ", values[i]); } printf("\n"); return 0; } #include <stdio.h> #include <stdlib.h> int compare_ints(const void *a, const void *b) { const int *ia = (const int *)a; const int *ib = (const int *)b; if (*ia < *ib) { return -1; } else if (*ia > *ib) { return 1; } else { return 0; } } int main(void) { int values[] = {7, 19, 3, 100, 5}; size_t count = sizeof values / sizeof values[0]; qsort(values, count, sizeof values[0], compare_ints); for (size_t i = 0; i < count; i++) { printf("%d ", values[i]); } printf("\n"); return 0; }](https://substackcdn.com/image/fetch/$s_!58_s!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8880990f-3eb4-4d29-8fdf-5a6ad5ac896a_1810x814.png)
![#include <stdio.h> #include <stdlib.h> struct record { int id; int score; }; int compare_records(const void *a, const void *b) { const struct record *ra = (const struct record *)a; const struct record *rb = (const struct record *)b; if (ra->score > rb->score) { return -1; /* higher score comes first */ } else if (ra->score < rb->score) { return 1; } else if (ra->id < rb->id) { return -1; } else if (ra->id > rb->id) { return 1; } else { return 0; } } int main(void) { struct record items[] = { {1, 90}, {2, 95}, {3, 90}, {4, 80} }; size_t count = sizeof items / sizeof items[0]; qsort(items, count, sizeof items[0], compare_records); for (size_t i = 0; i < count; i++) { printf("id=%d score=%d\n", items[i].id, items[i].score); } return 0; } #include <stdio.h> #include <stdlib.h> struct record { int id; int score; }; int compare_records(const void *a, const void *b) { const struct record *ra = (const struct record *)a; const struct record *rb = (const struct record *)b; if (ra->score > rb->score) { return -1; /* higher score comes first */ } else if (ra->score < rb->score) { return 1; } else if (ra->id < rb->id) { return -1; } else if (ra->id > rb->id) { return 1; } else { return 0; } } int main(void) { struct record items[] = { {1, 90}, {2, 95}, {3, 90}, {4, 80} }; size_t count = sizeof items / sizeof items[0]; qsort(items, count, sizeof items[0], compare_records); for (size_t i = 0; i < count; i++) { printf("id=%d score=%d\n", items[i].id, items[i].score); } return 0; }](https://substackcdn.com/image/fetch/$s_!TL27!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F95c5f69b-ad80-4ce7-bf92-7e3aec33f974_1785x1052.png)
![#include <stdio.h> typedef void (*event_handler)(int event_code); void handle_connect(int event_code) { printf("connect event %d\n", event_code); } void handle_disconnect(int event_code) { printf("disconnect event %d\n", event_code); } struct entry { int event_code; event_handler handler; }; void dispatch_event(struct entry *table, size_t count, int event_code) { for (size_t i = 0; i < count; i++) { if (table[i].event_code == event_code) { table[i].handler(event_code); return; } } printf("no handler for event %d\n", event_code); } int main(void) { struct entry handlers[] = { {1, handle_connect}, {2, handle_disconnect} }; dispatch_event(handlers, 2, 1); dispatch_event(handlers, 2, 2); dispatch_event(handlers, 2, 3); return 0; } #include <stdio.h> typedef void (*event_handler)(int event_code); void handle_connect(int event_code) { printf("connect event %d\n", event_code); } void handle_disconnect(int event_code) { printf("disconnect event %d\n", event_code); } struct entry { int event_code; event_handler handler; }; void dispatch_event(struct entry *table, size_t count, int event_code) { for (size_t i = 0; i < count; i++) { if (table[i].event_code == event_code) { table[i].handler(event_code); return; } } printf("no handler for event %d\n", event_code); } int main(void) { struct entry handlers[] = { {1, handle_connect}, {2, handle_disconnect} }; dispatch_event(handlers, 2, 1); dispatch_event(handlers, 2, 2); dispatch_event(handlers, 2, 3); return 0; }](https://substackcdn.com/image/fetch/$s_!5cWN!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6b43a8ec-9d3f-4d83-8092-09d7799a6ce2_1796x954.png)
