After wrapping your head around storage duration and scope in part 16, it's time to treat functions themselves like values so you can compose more flexible code. A function pointer stores the memory address where a function begins, and a callback is the pattern of passing that address to other code so it can invoke the function later.
New Terms in This Lesson
- Function Pointer: A pointer that stores the entry address of a function
- Callback: Passing a function as an argument and letting someone else call it later
- Signature: A function's return type and parameter list without the name
- Strategy Pattern: Injecting behavior chosen outside the callee
Key Ideas
Study Notes
- Estimated time: 60–75 minutes
- Prereqs: Pointer basics, declaring/defining functions, iterating arrays
- Goal: Declare/assign/invoke function pointers and design callback-based flows
When you see a function pointer, check the signature first. int (*handler)(const char *msg) can look intimidating, but you can unpack it in steps: the pointer name is handler, the pointed-to function takes const char *msg, and the return type is int. In the beginning, pretend it's "a variable that stores the location of a function instead of a number" and everything becomes simpler.
We'll tackle the following steps:
- Declare function pointers and understand why you can assign both
&funcandfunc - Group multiple function pointers in an array and iterate through them
- Accept callbacks as parameters to swap sorting strategies or event handlers
- Pass extra data with
void *contextso callbacks can interpret their environment
Code Walkthrough
Function Pointer Basics
#include <stdio.h>
int square(int n) {
return n * n;
}
int cube(int n) {
return n * n * n;
}
int main(void) {
int (*operation)(int);
operation = square;
printf("square(3) = %d\n", operation(3));
operation = cube;
printf("cube(3) = %d\n", operation(3));
return 0;
}
You can write operation = square; without the ampersand because function names decay to their starting addresses. Calling operation(3) jumps into whichever function the pointer currently references.
Arrays of Function Pointers
#include <stdio.h>
typedef int (*calc_fn)(int);
int add_one(int n) {
return n + 1;
}
int double_num(int n) {
return n * 2;
}
int negate(int n) {
return -n;
}
int main(void) {
calc_fn steps[3] = {add_one, double_num, negate};
int value = 5;
int i;
for (i = 0; i < 3; i++) {
value = steps[i](value);
printf("[%d] -> %d\n", i, value);
}
return 0;
}
typedef keeps declarations tidy. Here, an array named steps stores three operations. As long as they share the same signature, you can loop through them like any other array.
Switching Sort Strategies with Callbacks
#include <stdio.h>
typedef int (*compare_fn)(int a, int b);
int ascending(int a, int b) {
return (a > b) - (a < b);
}
int descending(int a, int b) {
return (b > a) - (b < a);
}
void selection_sort(int *arr, int len, compare_fn cmp) {
int i, j, min_idx;
for (i = 0; i < len - 1; i++) {
min_idx = i;
for (j = i + 1; j < len; j++) {
if (cmp(arr[j], arr[min_idx]) < 0) {
min_idx = j;
}
}
if (min_idx != i) {
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
}
int main(void) {
int data[5] = {42, 7, 98, 13, 55};
selection_sort(data, 5, ascending);
printf("ascending: %d %d %d %d %d\n",
data[0], data[1], data[2], data[3], data[4]);
selection_sort(data, 5, descending);
printf("descending: %d %d %d %d %d\n",
data[0], data[1], data[2], data[3], data[4]);
return 0;
}
Inside the sort function, the cmp callback decides ordering. Swap out the comparison and you get a different behavior while reusing the same sorting logic. The usual agreement is simple: return a negative value if the first argument should come first, 0 if the two values are equal, and a positive value otherwise.
Passing Context into Callbacks
#include <stdio.h>
typedef void (*log_fn)(const char *msg, void *context);
typedef struct {
int warning_threshold;
} logger_config;
void console_logger(const char *msg, void *context) {
logger_config *cfg = (logger_config *)context;
printf("[warn>= %d] %s\n", cfg->warning_threshold, msg);
}
void monitor_temperature(int readings[], int len, log_fn logger, void *ctx) {
int i;
logger_config *cfg = (logger_config *)ctx;
for (i = 0; i < len; i++) {
if (readings[i] >= cfg->warning_threshold) {
char buffer[64];
snprintf(buffer, sizeof(buffer), "sensor %d: %dC", i, readings[i]);
logger(buffer, ctx);
}
}
}
int main(void) {
int temps[6] = {32, 48, 51, 37, 60, 42};
logger_config cfg = {45};
monitor_temperature(temps, 6, console_logger, &cfg);
return 0;
}
If a callback receives only the address of a function, it lacks context. What if the callback needs to remember extra settings such as a warning threshold or a label? That is where void *context comes in: it is a small data bag the caller can pass along. The callback then casts it back to the proper type. Because void * carries no type information, both sides must agree on the structure, and casting it to the wrong type is a bug the compiler may not catch. This pattern is everywhere in GUI event loops and network libraries.
Why It Matters
- Function pointers build intuition that code can move around like data.
- Callbacks separate library code from user-provided behavior.
- You can implement the strategy pattern at C's level by injecting different operations.
- OS APIs, signal handlers, and timer libraries rely heavily on these patterns.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.
💬 댓글
이 글에 대한 의견을 남겨주세요