← Fundamentals
Guided lesson 7 questions · C only

C function pointers and explicit context

Declaring and calling through pointers, tables of functions, qsort’s compare function, and the context a callback needs.

A C function pointer holds the address of a function and nothing else. Behavior that needs data travels as two values, a function pointer and a context pointer, and the code that passes them must keep their types and lifetimes in step.

Written for C.

Before you begin

Why this matters

C libraries take behavior as a function pointer: qsort, timers, event handlers, and command tables all do. A function pointer of the wrong type, a wrong position in a table, or a context that no longer exists can compile and even run. You can find these bugs only by reading the code and knowing the rules.

What this helps you reason about

  • Read and write a declaration such as int (*rule)(int), and give its type a name with typedef.
  • Predict which function a table of function pointers calls, and what qsort passes to a compare function.
  • Spot a context converted to the wrong type, a context that no longer exists, and a call through NULL.

In Java, Python, and JavaScript, you can pass a function that keeps the variables it needs. Closures and captured variables showed that C has no closures: a C program pairs a function pointer with a void * context instead. This lesson goes further into that pair. It shows how to read and write function pointer types, how a table of them chooses a function by number, how qsort calls your compare function, how a library passes a context to a callback, and which calls through a function pointer are undefined.

Already know this? The short version
  • int (*rule)(int) declares a pointer to a function that takes an int and returns an int; rule(x) calls it, and typedef int (*Rule)(int); names the type.
  • An array of function pointers indexed by a number is a dispatch table. Position alone links a number to a function, and the number must be checked before the call.
  • qsort calls int compare(const void *a, const void *b). Convert both pointers back to the element type and return (x > y) - (x < y), not x - y.
  • A library passes a void * context to the callback unchanged. The callback must convert it to exactly the type the caller passed, and the data must still exist when the callback runs.
  • A call through a pointer of the wrong function type, or through NULL, is undefined behavior. Bugs from real programs are in “What goes wrong with function pointers in real programs?”.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
function pointer
A value that holds the address of a function, declared as in int (*run)(void *context). Calling run(x) calls the function that run points to.
typedef
A declaration that gives an existing type a second name, as in typedef int (*Programme)(int plates);. It creates no new type: Programme and int (*)(int) are the same type.
dispatch table
An array of function pointers indexed by a number, such as a button or a command code. table[i](x) calls the function stored at position i.
qsort
The sorting function from <stdlib.h>: qsort(array, count, size of one element, compare function). It sorts the array in place and calls the compare function with pointers to two elements.
void pointer
A pointer of type void *, which can hold the address of any data. It must be converted to a typed pointer, such as int *, before the data it points to can be used.
callback
A function passed to other code as a function pointer, so that the other code can call it later, such as the compare function that qsort calls.
context
The data a callback works on, passed to it as a void * pointer, so that one function can serve several callers, each with its own data.
undefined behavior
Code for which the C standard sets no requirements. The program may crash, print wrong results, or seem to work, and the result can change between builds.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Declaring

How do you declare and call a function pointer?

Function calls passed a function to apply_to through the parameter int (*step)(int). Every function pointer declaration has that shape: the return type, then (*name), then the parameter types. The parentheses around *name are needed. Without them, int *step(int) declares a function that returns an int *.

A function’s name, written without a call, gives the function’s address. So programme = eco; stores eco’s address, and programme(12) calls the function that programme points to at that moment. (*programme)(12) means the same thing, in an older style.

Function pointer types are hard to read, especially as parameters. typedef gives a type a second name: after typedef int (*Programme)(int plates);, the name Programme means “pointer to a function that takes an int and returns an int”. A typedef creates no new type; it only names one.

main.c
int quick(int plates) { return 30 + plates; }
int eco(int plates) { return 150 + plates; }

int (*programme)(int plates) = quick;
/* 42 */
int first = programme(12);
programme = eco;
/* 162 */
int second = programme(12);

typedef int (*Programme)(int plates);
/* the same type as programme */
Programme next = eco;

int (*name)(int) is a pointer to a function; name(x) calls whichever function it holds.

Coming from Java: A method reference such as Main::triple becomes an object that implements an interface, and it can carry data. A C function pointer is only an address with a function type; any data must travel beside it.

Precisely: C11 6.3.2.1p4, 6.5.2.2p1, and 6.3.2.3

6.3.2.1p4: a function designator, such as a function’s name in an expression, is converted to a pointer to the function, except under sizeof or unary &. So eco and &eco give the same pointer. 6.5.2.2p1: the expression before the parentheses of a call must have the type “pointer to function”, so programme(12) calls through the pointer directly. In (*programme)(12), the * gives a function designator, which is converted back to a pointer at once. ISO C defines conversions between object pointers and void * (6.3.2.3p1) and between function pointer types (6.3.2.3p8), but none between a function pointer and void *. POSIX requires that conversion to work, because dlsym returns a void *. In the stepper’s build, a function pointer is 8 bytes, the same as a data pointer.

Experiment

Point at one programme, then another

int (*programme)(int plates) makes programme a variable that holds the address of a function. quick, written without parentheses, gives quick’s address. programme(12) calls whichever function the variable holds at that moment, so the same line of code runs quick and then eco.

Why it matters Read a function pointer declaration from the name outwards: (*programme) is a pointer, (int plates) to a function that takes an int, and the int in front is what that function returns.

Predict the two numbers. Then remove the parentheses around *programme, so that the line reads int *programme(int plates) = quick;, and predict what the compiler says.

#include <stdio.h>

int quick(int plates) {
    return 30 + plates;
}

int eco(int plates) {
    return 150 + plates;
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    int (*programme)(int plates) = quick;
    int first = programme(12);
    programme = eco;
    int second = programme(12);
    printf("%d %d\n", first, second);
    return 0;
}
What to look for

After line 13 runs, programme appears in main’s frame as fn @ 0x…: the debugger shows a function pointer as an address, without the function’s name. Step into the call on line 14: quick’s frame appears above main, with plates 12. After line 15, programme holds a different address, eco’s. The call on line 16 enters eco. The run prints 42 162. After the edit, the build fails, and the first error is error: function ‘programme’ is initialized like a variable. Without the parentheses, the line declares a function that returns an int *, and a function cannot be given a value.

Expected output

42 162

02 / Dispatch tables

How do you choose a function by number?

An array of function pointers lets a number choose a function. The number can come from a button, a menu, or a command code. Such an array is called a dispatch table: table[button](plates) reads the pointer at position button and calls it.

The position is the only link between a number and a function. Nothing checks that the function at position 2 is the one meant for button 2, so the order of the entries is part of the program’s meaning.

C does not check the index either. With a number outside the table, table[button] reads whatever is stored after the array and calls it as a function. That is undefined behavior, so check the number before the call. sizeof table / sizeof table[0], the size of the whole array divided by the size of one entry, gives the number of entries, and it stays right when an entry is added.

main.c
typedef int (*Programme)(int plates);

int press(int button, int plates) {
    Programme table[] = {eco, quick, intensive};
    int count = sizeof table / sizeof table[0];
    if (button < 0 || button >= count) {
        return -1; /* no such button: call nothing */
    }
    return table[button](plates);
}

In a dispatch table, position alone links a number to a function; check the number before the call.

Coming from Python: A Python dict of functions, such as {"eco": eco}, fails with KeyError on an unknown key. A C array of function pointers has no such check: an unknown index reads past the array.

Precisely: designated initializers and const tables

C11 6.7.9p6 and p17: Programme table[3] = {[QUICK] = quick, [ECO] = eco}; places each entry at the index in brackets, whatever the order of the lines; this has been valid since C99. enum { ECO, QUICK, INTENSIVE }; names the int constants 0, 1, and 2, which suits table positions. 6.7.9p10 and p21: an entry that the initializer leaves out is a null pointer, so a gap gives NULL, not a neighbour’s function. A table that never changes can be declared static const Programme table[] = {…};. static makes one table for the whole run instead of building it on every call, and const lets the compiler place it in read-only memory. A writable table of function pointers is a known target for attacks that overwrite memory, because one changed entry redirects every later call.

Experiment

Choose a programme by button number

table[button] reads one function pointer from the array, and the parentheses after it call that function. count comes from sizeof, so it follows the table: 3 entries, then 4. The test on line 13 runs before any read of table, so button 3 never reads past a table of 3.

Why it matters Keep the bounds check next to the table, and compute the count from the table itself, so that adding an entry cannot leave the check behind.

Predict both numbers. Then add rinse to the end of the table, so that it reads {eco, quick, intensive, rinse}, and predict the second number again.

#include <stdio.h>

typedef int (*Programme)(int plates);

int eco(int plates) { return 150 + plates; }
int quick(int plates) { return 30 + plates; }
int intensive(int plates) { return 90 + 2 * plates; }
int rinse(int plates) { return 5 + plates; }

int press(int button, int plates) {
    Programme table[] = {eco, quick, intensive};
    int count = sizeof table / sizeof table[0];
    if (button < 0 || button >= count) {
        return -1; /* no such button: call nothing */
    }
    return table[button](plates);
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    int a = press(2, 10);
    int b = press(3, 10);
    printf("%d %d\n", a, b);
    return 0;
}
What to look for

Step into press. After line 11, table shows three entries, each an address followed by the function’s name in angle brackets: <eco>, <quick>, <intensive>. Unlike a single function pointer, an array of them shows the names. count shows 3. For button 2, the call on line 16 enters intensive with plates 10, and a becomes 110. For button 3, the test on line 13 is true, line 14 runs, and no programme’s frame appears; b becomes -1. The run prints 110 -1. After the edit, table shows four entries, count shows 4, button 3 enters rinse, and the run prints 110 15.

Expected output

110 -1

03 / qsort

How does qsort call your compare function?

qsort, from <stdlib.h>, sorts an array of any type. It cannot know the type of the elements, so you give it the array, the number of elements, the size of one element, and a compare function. qsort calls the compare function with the addresses of two elements and arranges the elements by its answers.

The compare function must have exactly this type: int compare(const void *a, const void *b). const means the function may read the elements but not change them. Inside, convert each pointer back to the element’s real type, as in const Load *x = a;. Then return a negative number when *x comes first, 0 when the two are equal, and a positive number when *y comes first.

Below, typedef struct { … } Load; defines a struct with two members and names it Load. by_minutes returns (x->minutes > y->minutes) - (x->minutes < y->minutes). Each comparison gives 1 or 0, so the result is -1, 0, or 1. Unlike x->minutes - y->minutes, it can never overflow; “What goes wrong with function pointers in real programs?” below shows why that matters.

main.c
typedef struct {
    const char *day;
    int minutes;
} Load;

int by_minutes(const void *a, const void *b) {
    const Load *x = a;
    const Load *y = b;
    return (x->minutes > y->minutes) - (x->minutes < y->minutes);
}

Load week[3] = {{"Mon", 162}, {"Tue", 42}, {"Wed", 110}};
/* Tue Wed Mon */
qsort(week, 3, sizeof week[0], by_minutes);

qsort passes two const void * pointers; convert them back to the element type and return a negative number, 0, or a positive number.

Coming from Java: A Comparator<Load> receives two Loads, and the compiler checks their type. qsort’s compare function receives two untyped addresses, and nothing checks that you convert them to the right type.

Precisely: C11 7.22.5 and 7.22.5.2

7.22.5p4: when the same two elements are compared more than once, the answers must agree, and for qsort they must define one total order. A compare function that breaks this, such as one that returns 1 whenever two elements differ, makes the call undefined, not only the order wrong. 7.22.5.2p4: equal elements end in an unspecified order, so qsort is not stable; do not rely on a test that shows equal elements keeping their order. The standard does not say which pairs qsort compares or how often. A compare function written as int by_minutes(const Load *, const Load *) and cast to qsort’s type compiles without a warning, even with -Wextra, but a call through the other type is undefined (6.3.2.3p8); write the const void * version and convert inside. Ordering, sorting, and comparators, a later lesson, covers what a compare function promises in every language.

Experiment

Watch qsort call the compare function

qsort knows only the address of week, the count 3, and the size of one element. It chooses two elements, calls by_minutes with their addresses, and arranges the elements by the answers. by_minutes is the only code that knows the elements are Loads.

Why it matters The compare function alone decides the order. Reversing its sign reverses the sort, with no change to the qsort call.

Predict the printed order of the days. Then swap the two comparisons in the return line, so that it reads return (x->minutes < y->minutes) - (x->minutes > y->minutes);, and predict again.

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    const char *day;
    int minutes;
} Load;

int by_minutes(const void *a, const void *b) {
    const Load *x = a;
    const Load *y = b;
    return (x->minutes > y->minutes) - (x->minutes < y->minutes);
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    Load week[3] = {{"Mon", 162}, {"Tue", 42}, {"Wed", 110}};
    qsort(week, 3, sizeof week[0], by_minutes);
    printf("%s %s %s\n", week[0].day, week[1].day, week[2].day);
    return 0;
}
What to look for

Step from line 18 into by_minutes. qsort itself never appears in the frame list: the stepper stops only in the program’s own functions. by_minutes is entered three times, and each time a and b show two addresses. After lines 10 and 11, x and y show the same addresses, followed by → and the Load stored there: Tue 42 against Wed 110, then Mon 162 against Tue 42, then Mon 162 against Wed 110. Which pairs qsort compares, and in which order, is chosen by the C library. After line 18, week starts with Tue. The run prints Tue Wed Mon. After the edit, by_minutes is entered only twice in this build, and the run prints Mon Wed Tue.

Expected output

Tue Wed Mon

04 / Context

How does a callback get the data it needs?

Closures and captured variables paired a function pointer with a context pointer by hand. A library function that takes a callback uses the same pair, as two parameters. A callback is a function passed to other code as a function pointer, so that the other code can call it later. The usual shape is a function pointer parameter, followed by a void *context parameter that the library passes to every call of the callback without reading it.

Below, each_load walks an array of minutes and calls visit for each one. each_load knows nothing about Totals. add_load converts context back to Totals *, the type that main passed, and changes main’s struct through it. Another caller can pass other data, together with a callback that converts the context to that other type.

The library also decides what a callback’s return value means. Here Visit returns void, so there is nothing to decide. When a callback returns a value, read what the library says it does with that value, not only the callback’s type.

main.c
typedef void (*Visit)(int minutes, void *context);

void each_load(const int *loads, size_t n, Visit visit,
               void *context) {
    for (size_t i = 0; i < n; i++) {
        visit(loads[i], context);
    }
}

void add_load(int minutes, void *context) {
    Totals *totals = context;
    totals->count = totals->count + 1;
    totals->total = totals->total + minutes;
}

Totals totals = {0, 0};
each_load(week, 3, add_load, &totals);

A library passes the context through unchanged; only the callback knows its real type.

Coming from TypeScript: A TypeScript callback uses the outer variables it needs directly. In C, the caller must pass that data as a context, and the callback must convert it back to the right type itself.

Precisely: C11 6.3.2.3p1 and const contexts

6.3.2.3p1: a pointer to any object type may be converted to void * and back, and the result compares equal to the original. The guarantee covers the way back to the original type. Converting to another struct type and reading through it is undefined behavior, and the compiler cannot check it, because void * converts to every object pointer type without a cast. When a callback only reads, a const void *context parameter lets the compiler reject a write through it. Libraries differ in where the context goes: pthread_create passes a single void * to the thread’s start function, and qsort passes none, so a compare function that needs extra data must read a variable declared outside every function. glibc’s qsort_r adds a context as the last argument, and macOS’s qsort_r passes it in a different position, so code that uses either is not portable.

Experiment

Pass a context through a library function

each_load stores nothing. It passes the same context address to every call of visit. add_load converts that address back to Totals *, the type main passed, and changes main’s struct through it. All the counting happens in main’s totals, so a second walk continues where the first one ended.

Why it matters The state lives in the context, not in the callback or in the library. To start again from zero, the caller must pass fresh data.

Predict the printed line. Then add a second copy of the line each_load(week, 3, add_load, &totals); directly below the first one, and predict again.

#include <stdio.h>
#include <stddef.h>

typedef void (*Visit)(int minutes, void *context);

void each_load(const int *loads, size_t n, Visit visit,
               void *context) {
    for (size_t i = 0; i < n; i++) {
        visit(loads[i], context);
    }
}

typedef struct {
    int count;
    int total;
} Totals;

void add_load(int minutes, void *context) {
    Totals *totals = context;
    totals->count = totals->count + 1;
    totals->total = totals->total + minutes;
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    int week[3] = {162, 42, 110};
    Totals totals = {0, 0};
    each_load(week, 3, add_load, &totals);
    printf("%d loads, %d minutes\n", totals.count, totals.total);
    return 0;
}
What to look for

Step into each_load. visit shows fn @ 0x…, and context shows an address. loads shows week’s address and → 162, its first element, on every pass, because loads itself never moves. Step into add_load: its context shows the same address as each_load’s, and after line 19, totals shows that address followed by → {count: 0, total: 0}. After lines 20 and 21, count and total change in add_load’s totals and in main’s totals at the same stop: they are one struct. The run prints 3 loads, 314 minutes. After the edit, the second walk starts from {count: 3, total: 314}, and the run prints 6 loads, 628 minutes.

Expected output

3 loads, 314 minutes

05 / Undefined calls

When is a call through a function pointer undefined?

A call through a function pointer is defined only when three things are true.

The pointer’s type matches the function’s type: the same return type and the same parameter types. When they differ, gcc warns, for example: initialization of ‘int (*)(int)’ from incompatible pointer type ‘int (*)(int, int)’. A cast, as in (int (*)(int))two, removes the warning but not the problem: calling a function through a pointer of another function type is undefined behavior. Change the function or the pointer’s type instead.

The pointer points to a function. NULL, the null pointer, is a function pointer value that C programs use for “no function”. A static function pointer starts as NULL, and so does a table entry that the initializer leaves out. Calling through NULL is undefined behavior; on Linux it usually stops the program with a segmentation fault. Test the pointer first, as run_load does below.

The context still exists. A local variable ends when its function returns, so a context that points to it must not be used afterwards. gcc warns about return &glasses; with function returns address of local variable. It says nothing when the same address is stored in a struct that is returned.

main.c
typedef void (*Done)(int minutes);

int run_load(int plates, Done done) {
    int minutes = 30 + plates;
    if (done != NULL) {
        done(minutes);
    }
    return minutes;
}

/* no beep: done is NULL */
int first = run_load(12, NULL);

Call through a function pointer only when it has exactly the function’s type, is not NULL, and its context still exists.

Coming from Java: A call on a null reference throws NullPointerException, which a program can catch. A C call through NULL is undefined behavior, so the test must come before the call.

Precisely: C11 6.3.2.3p8, 6.5.2.2p9, and the warnings

6.3.2.3p8: a pointer to a function may be converted to another function pointer type and back again, and the result compares equal to the original; calling the function while the pointer has the other type is undefined. 6.5.2.2p9 says the same for every call through a type not compatible with the function’s. The warning above is -Wincompatible-pointer-types, a warning in the stepper’s gcc 12 and an error by default since gcc 14. -Wcast-function-type, part of -Wextra, does not report a cast that changes only the pointer types of the parameters, such as const Load * to const void *. 6.3.2.3p3: a null pointer compares unequal to a pointer to any function. On a small microcontroller, a call through NULL may not stop the program: on an AVR, address 0 is the reset vector, so the call restarts the program without any error.

Experiment

Skip a callback that is NULL

NULL is a function pointer value that points to no function. run_load tests done before the call, so a caller can pass NULL to mean “no callback”. A call through NULL, without the test, would be undefined behavior.

Why it matters Test every function pointer that may be NULL before calling it. Another choice is to store a function that does nothing, so that every call is safe.

Predict all the output, including which call beeps. Then change run_load(12, NULL) to run_load(12, beep) and predict again.

#include <stdio.h>

typedef void (*Done)(int minutes);

void beep(int minutes) {
    printf("beep after %d minutes\n", minutes);
}

int run_load(int plates, Done done) {
    int minutes = 30 + plates;
    if (done != NULL) {
        done(minutes);
    }
    return minutes;
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    int first = run_load(12, NULL);
    int second = run_load(10, beep);
    printf("%d %d\n", first, second);
    return 0;
}
What to look for

Step into the first call of run_load. done shows fn @ 0x0: the null pointer. The test on line 11 is false, line 12 is skipped, and no beep frame appears. In the second call, done shows an address, the call on line 12 puts beep’s frame above run_load with minutes 40, and the output gains beep after 40 minutes. The run prints beep after 40 minutes, then 42 40. After the edit, both calls enter beep, and the run prints beep after 42 minutes, beep after 40 minutes, and 42 40.

Expected output

beep after 40 minutes
42 40

06 / In production

What goes wrong with function pointers in real programs?

The rules in this lesson explain four bugs that C programmers meet in real programs.

A context converted to the wrong type. A program registers two callbacks, that is, hands them to a library to call later: one with &totals and one with &settings. One callback converts its context to the other struct type. void * converts to any pointer type without a cast, so nothing warns, and the callback reads the wrong bytes: undefined behavior. Keep each callback with the one context type it expects, for example in one struct, and convert the context in the callback’s first line.

A context that has ended. A function registers a timer with the address of its own local, then returns. When the timer runs, the local no longer exists. The usual repair is a variable that lives longer, such as a static one.

A table indexed by unchecked input. A code read from a file or a message and used as table[code]() calls whatever address lies outside the table when the code is negative or too large. Check 0 <= code && code < count, with count computed by sizeof, before the call.

A compare function that subtracts. return x - y; overflows for ints far apart, such as INT_MIN and 1, which is undefined behavior, as Integers, signs, and overflow showed. For doubles, return (int)(x - y); drops the fraction, so 0.9 and 1.1 compare as equal. Write (x > y) - (x < y). The experiment below shows the double case.

main.c
/* wrong: overflows for ints, truncates doubles */
return (int)(*x - *y);
/* right for every number type */
return (*x > *y) - (*x < *y);

Convert a context to the type it was given, keep it alive, check a table index, and compare instead of subtracting.

Precisely: what the tools catch

-fsanitize=undefined, in gcc and clang, reports a signed overflow in a compare function when it happens. Since clang 17, -fsanitize=function also reports, in C, a call through a pointer of the wrong function type. No tool reports a void * context converted to the wrong struct type: the conversion is always allowed, and the bytes it reads usually belong to the program. An attacker who controls an unchecked table index can sometimes choose the address that is called, which is why the check belongs before every call through a table.

Experiment

Compare two energy values by subtracting

Converting a double to int drops the fractional part, so every difference smaller than 1 becomes 0. Given to qsort, this compare function would break qsort’s rule that the answers form one consistent order, which is undefined behavior; the experiment calls it directly instead. Each comparison in the edit gives 1 or 0, so their difference is -1, 0, or 1, with no conversion.

Why it matters A compare function should compare, not subtract. (x > y) - (x < y) works the same for ints, doubles, and unsigned values.

Predict the number printed for 0.9 kWh against 1.1 kWh. Then change the return line to return (*x > *y) - (*x < *y); and predict again.

#include <stdio.h>

int by_energy(const void *a, const void *b) {
    const double *x = a;
    const double *y = b;
    return (int)(*x - *y);
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    double eco = 0.9;
    double quick = 1.1;
    int order = by_energy(&eco, &quick);
    printf("%d\n", order);
    return 0;
}
What to look for

Step into by_energy. x shows an address followed by → 0.9, and y → 1.1. *x - *y is about -0.2, and converting it to int drops the fraction, so order shows 0 after line 13: the compare function says that 0.9 and 1.1 are equal. The run prints 0. After the edit, order shows -1, and the run prints -1.

Expected output

0

07 / Try it

Can you follow each call through the table?

Read the declaration of steps from the name outwards: it is an array, written without a typedef.

main.c
#include <stdio.h>

int add_minutes(int minutes, void *context) {
    int *extra = context;
    return minutes + *extra;
}

int main(void) {
    setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
    int dry = 20;
    int hot = 45;
    int (*steps[3])(int, void *) = {add_minutes, NULL, add_minutes};
    void *contexts[3] = {&dry, NULL, &hot};
    int minutes = 100;
    for (int i = 0; i < 3; i++) {
        if (steps[i] != NULL) {
            minutes = steps[i](minutes, contexts[i]);
        }
    }
    dry = 0;
    printf("%d\n", minutes);
    return 0;
}
Precisely: the same table with typedef

typedef int (*Step)(int minutes, void *context); followed by Step steps[3] = {add_minutes, NULL, add_minutes}; declares the same array. Most C code bases name function pointer types this way, and some typedef the function type instead (typedef int Step(int, void *);) and write Step *steps[3], so that the * stays visible where the pointer is declared.

What does it print? What would it print if the line dry = 0; moved up, above the for loop?

Reveal the reasoning

165. steps[3] is an array of 3; (*…) makes each element a pointer; (int, void *) makes it a pointer to a function that takes an int and a void * and returns an int. The middle entry is NULL. The loop calls add_minutes with &dry (100 + 20 = 120), skips the NULL entry, and calls add_minutes with &hot (120 + 45 = 165). dry = 0; runs after the loop, so no call sees it. If it moved above the loop, the first call would read 0 through its context, and the run would print 145.

Practice tickets

  1. C-301 · The night button runs the fan at full speed → Free
  2. C-302 · The night summary finds at most one plug left on → Sign in · Pro
  3. C-303 · Every blind opens like the last one entered → Sign in · Pro

Keep exploring

Arrays, lists, and vectors →

Primary references