C Part I · Module 7 · Pointers
Function Pointers
This module has spent twelve chapters on pointers to data, and a function is not data — Module 2 defined an object as storage with a type, and a function is not one of those. Yet a function plainly occupies a position in a running program, since a call has to find it, and Chapter 7.12 hinted that a pointer to one is a different kind of thing rather than an impossible one. It is, and almost everything about it is a rule you already hold: the name converts to a pointer in the same way an array name does, the declaration needs parentheses for the same precedence reason an array of pointers does, and an array of them is the previous structure with a different element type. The notation is genuinely unpleasant to read, and this chapter is honest about that. What it buys is two patterns that are difficult to express any other way.
Advanced17 min readCPart IFundamentalsPointersFunctions
Part I · Module 7 · Chapter 7.13 · Function Pointers
1. Declaring One
int (*operation)(int, int);Read it with Chapter 7.9 §3's technique — start at the name, go right while you can, then left, with parentheses overriding:
operation start at the name
(*operation) the parentheses force left first: a pointer
(*operation)(int, int) right: to a function taking two ints
int (*operation)(int, int) left: returning int
-> operation is a pointer to a function
taking two ints and returning intThe parentheses around *operation are not optional, and the reason is the one from Chapter 7.9 §3: the trailing (...) binds tighter than the leading *, exactly as [] does.
int (*operation)(int, int); /* a pointer to a function */
int *wrong(int, int); /* a FUNCTION returning int * */2. Assigning and Calling
#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;
printf("operation(7, 3) with add %d\n", operation(7, 3));
printf("(*operation)(7, 3) same thing %d\n", (*operation)(7, 3));
operation = &subtract;
printf("operation(7, 3) with subtract %d\n", operation(7, 3));
printf("add == &add ? %d\n", add == &add);
printf("sizeof operation %zu\n", sizeof operation);
return 0;
}Output:
operation(7, 3) with add 10
(*operation)(7, 3) same thing 10
operation(7, 3) with subtract 4
add == &add ? 1
sizeof operation 8Four facts, and three of them should feel familiar.
operation = add; works without &. A function name used as a value converts to a pointer to that function — the same shape as Chapter 7.6's array conversion, and for the same reason: the name is not a value on its own, so the language supplies one.
add == &add is 1, which is the proof of that conversion. Both spellings produce the same pointer, so operation = &subtract; is equally valid and means exactly the same thing. Most C writes the bare name.
operation(7, 3) and (*operation)(7, 3) both work and both give 10. The second is the form that matches the type literally — dereference to get the function, then call it. The first is allowed because a call through a pointer is defined directly, and it is what almost everyone writes.
sizeof operation is 8 — it is a pointer, and an ordinary object like any other.
Calling through a null function pointer is undefined behaviour, exactly as Chapter 7.4 established for data, so a function pointer that could be unset gets the same guard.
3. Pattern One: Callbacks
A function that takes a function pointer as a parameter can have part of its behaviour supplied by its caller.
#include <stdio.h>
#include <stddef.h>
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int apply_all(int (*operation)(int, int), const int *values, size_t count)
{
if (operation == NULL || count == 0u) {
return 0;
}
int result = values[0];
for (size_t i = 1; i < count; i++) {
result = operation(result, values[i]);
}
return result;
}
int main(void)
{
int values[4] = {10, 2, 3, 1};
size_t n = sizeof values / sizeof values[0];
printf("fold with add %d\n", apply_all(add, values, n));
printf("fold with subtract %d\n", apply_all(subtract, values, n));
printf("fold with multiply %d\n", apply_all(multiply, values, n));
return 0;
}Output:
fold with add 16
fold with subtract 4
fold with multiply 6010 + 2 + 3 + 1 is 16, 10 - 2 - 3 - 1 is 4, and 10 * 2 * 3 * 1 is 60. One traversal, written once, doing three different things.
Every other part of that function is Module 7 material you already have. The const int *values, size_t count pair is Chapter 7.7's array parameter with Chapter 7.11's qualifier. The operation == NULL guard is Chapter 7.4 §5 applied to a function pointer, and it belongs there for exactly the same reason — the pointer came from a caller this function does not control.
A callback moves a decision from the function to its caller. The traversal knows how to walk the data; the caller knows what to do with each pair.
This is how the standard library's sorting and searching routines work — you supply a function that compares two items, and the library supplies the algorithm. Chapter 12.9 covers them.
4. Pattern Two: Dispatch Tables
Chapter 7.9 built an array of pointers to data. An array of pointers to functions is the same structure with a different element type.
int (*table[3])(int, int) = { add, subtract, multiply };
for (size_t i = 0; i < 3u; i++) {
printf("table[%zu](6, 3) = %d\n", i, table[i](6, 3));
}
printf("sizeof table %zu, element %zu\n", sizeof table, sizeof table[0]);table[0](6, 3) = 9
table[1](6, 3) = 3
table[2](6, 3) = 18
sizeof table 24, element 8Twenty-four bytes, three elements of eight — Chapter 6.1's contiguity result, with a function pointer as the element type. Nothing about the array is special.
Read the declaration with §1's procedure:
table start at the name
table[3] right: an array of 3
(*table[3]) left: of pointers
(*table[3])(int, int) right: to functions taking two ints
int (*table[3])(int, int) left: returning inttable[i](6, 3) is two steps: index the array to get a pointer, then call through it.
A dispatch table replaces a chain of branches with an index. An opcode, a message type or a command number becomes a subscript, and adding a case means adding a table entry rather than another branch.
Two cautions make it safe. The index must be validated against the element count before use — Chapter 6.4's rule, and here the consequence of getting it wrong is a call through whatever was at that position. And entries can be NULL, deliberately, to mean not implemented, which the caller must then check exactly as in §3.
5. The Notation Is the Real Cost
A signature that takes a callback and returns one is where this becomes genuinely hard to read:
int (*choose(int code))(int, int);That is a function choose, taking an int, returning a pointer to a function taking two int and returning int. It is correct, it is standard C, and almost nobody enjoys reading it.
C has a tool that fixes this — a way to give a type a name, so the declaration above becomes two readable lines instead of one dense one. It is Module 10's subject, and it is the normal way function-pointer-heavy code is written in practice.
Until then, two habits keep the raw notation manageable:
Read every one back using §1's procedure, as the callout recommended.
Keep them shallow. A pointer to a function is fine. A function returning a pointer to a function is where a reader starts needing a pencil, and is usually a sign that the type deserves a name.
6. Function Pointers Are Not Object Pointers
Chapter 7.12 §6 flagged this and it is worth closing properly, because it is the one place where this chapter's rules diverge from the rest of the module.
A function is not an object. Module 2 defined an object as storage with a type that holds a value; a function is executable code with an entry point. That is why a function pointer is a separate category rather than just another T *.
Three consequences:
void * does not portably hold one. Chapter 7.12's implicit conversions are specified for object pointer types. Converting a function pointer to void * is not guaranteed by C, even though it happens to work on most mainstream platforms — one more instance of Chapter 1.3's distinction.
There is no pointer arithmetic. Chapter 7.8's p + 1 means one element further on, which requires sizeof *p, and a function has no size. sizeof applied to a function is not valid.
Dereferencing behaves unusually. *operation yields the function, which immediately converts back to a pointer — which is why (*operation)(7, 3) and (**operation)(7, 3) and operation(7, 3) all do the same thing. This is a curiosity, not a technique.
7. What You Should Be Able to Say Now
int (*operation)(int, int);is a pointer to a function taking twointand returningint.- The parentheses are required —
int *wrong(int, int);compiles cleanly and declares a function. - Read every one back with the name-outward procedure before moving on.
- A function name converts to a pointer when used as a value, so
operation = add;needs no&. add == &addis 1, which is that conversion, observable.operation(7, 3)and(*operation)(7, 3)are equivalent; write the first.- A callback moves a decision from the function to its caller — one traversal, three behaviours.
- A dispatch table is Chapter 7.9's array of pointers with functions as targets;
table[i](6, 3)indexes then calls. - Null-check a function pointer from a caller, and validate a table index, before calling.
- A function is not an object: no portable
void *conversion, no arithmetic, nosizeof.
8. Exercises
Exercise 1 — read the declarations. Say in words what each declares.
void (*a)(int);
int (*b)(void);
double (*c[4])(double, double);
int *d(int);Exercise 2 — spot the non-pointer. One of these is not a function pointer. Say which, what it is instead, and whether the compiler would complain.
int (*p)(int);
int *q(int);
void (*r)(void);
char *(*s)(int);Exercise 3 — predict the output. Do not compile yet, given add, subtract and multiply as in §3.
int (*op)(int, int) = multiply;
printf("%d\n", op(4, 5));
op = add;
printf("%d\n", (*op)(4, 5));
printf("%d\n", op == add);
printf("%d\n", op == multiply);Exercise 4 — trace the fold. For apply_all(subtract, values, 4) with values = {10, 2, 3, 1}, give the value of result after each pass, and say why the loop starts at index 1.
Exercise 5 — write a callback. Write int count_matching(const int *values, size_t count, int (*matches)(int)) which counts the elements for which matches returns non-zero, plus one predicate function is_even. State what the function must do if matches is NULL.
Exercise 6 — build a table. Build a dispatch table of three operations and write the lookup that handles a command code which may be out of range. Say which two checks the lookup needs and which chapters they come from.
Exercise 7 — explain the equivalences. Say why operation(7, 3), (*operation)(7, 3) and (**operation)(7, 3) all do the same thing, and which you would write.
Exercise 8 — classify four claims. For each, say whether it is accurate and correct it if not.
- a function pointer can be stored in a
void *portably sizeofa function pointer is the same assizeofthe functionop + 1points at the next function- a function name used as a value yields a pointer to that function
9. Summary
int (*op)(int, int);is a pointer to a function; the parentheses are load-bearing, and omitting them declares a function instead — with no error.- Read every declaration back from the name outward.
- A function name converts to a pointer when used as a value;
add == &addis 1. op(a, b)and(*op)(a, b)are the same; write the first.- A callback supplies behaviour from the caller — one traversal, three results.
- A dispatch table is Chapter 7.9's structure with functions at the targets; index, then call.
- Null-check a caller's function pointer and validate a table index.
- The notation is the real cost, and Module 10's naming tool is the fix.
- A function is not an object: no portable
void *, no arithmetic, nosizeof.
Module 7 is complete. A pointer is an object whose value is the address of another object; its type says how wide the target is and how to read it; & and * move between the two; NULL means nothing is identified; passing an address lets a function reach a caller's object; an array name converts to a pointer, which is why a length never travels with it; arithmetic counts elements; and the same type can identify pointers, functions, or nothing in particular. Everything Modules 5 and 6 had to leave unfinished is now finished. What has not been examined once in thirteen chapters is where the objects at the far end of all these pointers actually come from, how long they last, and what happens to a pointer whose target has stopped existing — which is the subject of the next module.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
