student@ubuntu:~$
advanced-c Lesson 13 10 min read

Function Pointers

Passing behavior as data — C's version of callbacks and lambdas

Reading: Supplemental (not in textbook)

Quick check before you start: In Java, you can pass a Comparator to Collections.sort(). That is a function pointer in disguise. C does the same dispatch with raw function-pointer syntax, which this lesson walks through.

Practice this topic: Function Pointers skill drill

After this lesson, you will be able to:

  • Declare a function pointer variable
  • Use typedef to create readable function pointer aliases
  • Write a function whose return type is a function-pointer typedef (the bind pattern)
  • Write a comparator callback for sorting
  • Use qsort from the C standard library
  • Use a function pointer as a struct field for runtime dispatch

Two terms used throughout:

  • Function pointer: a variable whose value is the address of a function. The variable can be reassigned to point at a different function later.
  • Callback: a function you hand to other code (a library, a struct field, an event loop) so that code can call you back at the right moment. qsort’s comparator argument is the canonical example.

Declaration Syntax

A function pointer stores the address of a function. The syntax takes practice to read:

int (*compare)(int, int);

Read it from the inside out: compare is a pointer to a function that takes two int parameters and returns an int.

The parentheses around *compare are critical. Without them, int *compare(int, int) declares a function that returns int* — a completely different thing.

Assigning and Calling

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int);

op = add;
printf("%d\n", op(3, 4));  // prints 7

op = sub;
printf("%d\n", op(3, 4));  // prints -1

Bare name vs. the call (this is the trap)

This is the single most counter-intuitive rule in this lesson. Every other function in C you have ever written needed parentheses to invoke it. A function-pointer assignment breaks that rule.

add           // type: int (*)(int, int)   the address of the function
add(2, 3)     // type: int                 the result of running the function

The two expressions look almost identical and have entirely different types. The bare name add is what you assign to a function-pointer variable; add(2, 3) is what you write when you want the number 5. Calling through the pointer (op(2, 3)) means following the address stored in op and running whatever function lives there.

Many function-pointer bugs come from writing the wrong one of these two forms.

typedef Makes It Readable

The raw syntax is hard to read and easy to get wrong. Use typedef:

typedef int (*Comparator)(const void*, const void*);

Now Comparator is a type you can use for variables and parameters:

void sort(int *arr, int n, Comparator cmp);

Much cleaner than embedding the full pointer syntax in every parameter list.

A function that returns a function pointer

A function pointer can be the return type of another function, not just a variable’s type or a parameter. The pattern shows up whenever code needs to decide at runtime which function to use, then hand that choice to a caller:

typedef int (*Operation)(int, int);

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

// Pick which operation based on a mode flag.
Operation pick_op(int mode) {
    if (mode == 0) return add;
    return sub;
}

The return type of pick_op is Operation, the function-pointer type the typedef above defined. The line return add; returns the address of add (bare name, no parens), exactly the same expression you would write on the right side of op = add;.

The caller stores the returned address like any pointer and calls through it:

Operation op = pick_op(0);
printf("%d\n", op(10, 3));   // prints 13 (op is add)

op = pick_op(1);
printf("%d\n", op(10, 3));   // prints 7  (op is sub)

Picking at random

The same shape works with rand() instead of a mode parameter:

Operation pick_op_random(void) {
    int roll = rand() % 101;
    if (roll < 50) return add;
    return sub;
}

One roll, then partition the range [0, 100] with if / else if / else to choose one function from a fixed set. This is the bind pattern you will use in Lab 5: a function declared to return a function-pointer type, rolling rand() % 101, and returning one of several function-name addresses based on which band the roll lands in. The exact thresholds are your design choice. (rand() is declared in <stdlib.h>. The driver of any program that uses it calls srand(time(0)) once at startup so you do not have to seed it yourself.)

Writing a Comparator

A comparator takes two elements and returns:

  • Negative if the first should come before the second
  • Zero if they are equal
  • Positive if the first should come after the second
int compare_ints(const void *a, const void *b) {
    int x = *(const int*)a;
    int y = *(const int*)b;
    if (x < y) return -1;
    if (x > y) return 1;
    return 0;
}

The const void* parameters are required by qsort. You cast them to the actual type inside the function.

Why not return x - y;? It looks shorter and works for small numbers. The bug shows up at extremes: INT_MIN - 1 overflows to INT_MAX, flipping the sign of the comparison. Real comparator code uses the explicit form above (or the equivalent idiom (x > y) - (x < y)).

Using qsort

qsort is the C standard library’s sorting function. It sorts any array if you give it a comparator:

#include <stdlib.h>

int arr[] = {5, 2, 8, 1, 9};
int n = sizeof(arr) / sizeof(arr[0]);

qsort(arr, n, sizeof(int), compare_ints);
// arr is now {1, 2, 5, 8, 9}

The four arguments:

  1. Pointer to the array
  2. Number of elements
  3. Size of each element
  4. Comparator function

qsort does not know what type your array holds. It uses the size to move bytes around and the comparator to decide ordering. This is generic programming in C.

Reverse Order

To sort descending, flip the comparison:

int compare_ints_desc(const void *a, const void *b) {
    int x = *(const int*)a;
    int y = *(const int*)b;
    if (x > y) return -1;
    if (x < y) return 1;
    return 0;
}

Or call the ascending comparator and negate its result. Same data, different behavior, controlled by the function pointer you pass.

Sorting Structs

qsort works on any array, including arrays of structs. Here is a comparator that sorts students by GPA in ascending order:

typedef struct {
    char name[50];
    double gpa;
} Student;

int compare_by_gpa(const void *a, const void *b) {
    const Student *s1 = (const Student*)a;
    const Student *s2 = (const Student*)b;

    if (s1->gpa < s2->gpa) return -1;
    if (s1->gpa > s2->gpa) return 1;
    return 0;
}

The comparator casts the void* pointers to Student*, then compares the gpa fields. Do not write return (int)(s1->gpa - s2->gpa);: subtracting two doubles yields a double, and truncating that to int collapses any GPA difference smaller than 1.0 to zero. The sort then reports false ties and the order is wrong. Use the explicit if/else pattern above.

Student roster[] = {{"Alice", 3.7}, {"Bob", 2.9}, {"Carol", 3.4}};
size_t n = sizeof(roster) / sizeof(roster[0]);
qsort(roster, n, sizeof(Student), compare_by_gpa);
// roster is now: Bob (2.9), Carol (3.4), Alice (3.7)

To sort by a string field like name, use strcmp inside the comparator:

int compare_by_name(const void *a, const void *b) {
    const Student *s1 = (const Student*)a;
    const Student *s2 = (const Student*)b;
    return strcmp(s1->name, s2->name);
}

strcmp already returns negative, zero, or positive, so you can return its result directly.


Check Your Understanding
Which declaration correctly creates a function pointer named fp that points to a function taking two ints and returning an int?
Aint *fp(int, int);
Bint (*fp)(int, int);
Cint (fp*)(int, int);
D(*int)(fp)(int, int);
Answer: B. The parentheses around *fp are what make it a pointer to a function rather than a function returning a pointer. Option A declares a function named fp that returns int*.

Function Pointer as a Struct Field

Where we are. The sections above covered function pointers as qsort callbacks: the canonical introduction. The section below covers the same mechanism storing function pointers inside struct fields. Same machinery, different storage. This is the pattern Lab 5 uses.

A function pointer can also be a field inside a struct, just like an int or a char * can be a field. The two comparators in the qsort section (compare_by_gpa, compare_by_name) lived outside any struct; here, each struct instance carries one pointer of its own.

Why this matters: each instance of the struct dispatches to its own function. No switch (type) statement, no chain of if/else on a type tag. The call site reads the field, jumps to the address it finds there, and runs that body. Different instances reach different bodies through the same line of source code.

A small non-game example shows the shape. The add and sub functions from the “Assigning and Calling” section earlier on this page are reused below.

typedef int (*Operation)(int, int);

struct Calculator {
    int left;
    int right;
    Operation op;
};

Two instances, same struct, different behavior:

struct Calculator c1 = {10, 3, add};
struct Calculator c2 = {10, 3, sub};

printf("%d\n", c1.op(c1.left, c1.right));  // prints 13
printf("%d\n", c2.op(c2.left, c2.right));  // prints 7

The call sites c1.op(...) and c2.op(...) look identical in source, but each instance reaches a different function because the op field holds a different address.

The Java Analogue (and What Is Different)

If you have built a Java class that implements an interface, you have already used this pattern. The Java equivalent:

interface Operation {
    int apply(int a, int b);
}

class Calculator {
    int left;
    int right;
    Operation op;
}

Each Calculator instance carries a reference to an Operation object. Calling c.op.apply(...) reads the right implementation through the JVM’s vtable.

The mechanism is the same: an address, an indirect call, different behavior per instance. The difference is what the compiler verifies for you:

  Java C
Declares the call shape interface Operation { int apply(...); } typedef int (*Operation)(int, int);
Compiler enforces a body exists YES: class implementing the interface must provide apply NO: only checks that the static type matches
Call site reaches a real body Guaranteed (or NullPointerException with a stack trace) Not guaranteed: NULL, uninitialized, or stale fields crash as a segmentation fault
Wrong-body bug (returns the wrong number) Possible: compiler cannot verify behavior Possible: compiler cannot verify behavior

Java guarantees the method exists. C guarantees only that the signature type lines up. Neither language can verify that the body actually does what the contract promises; that responsibility is on you in both languages.

Where You’ll See This

You will see this pattern in Lab 5: each Entity (a struct representing a hero or monster) will carry a function-pointer field for its attack. Different entities get different attacks bound when the entity is built, the battle loop calls through that field, and one source line dispatches to many functions.

A small style note: Lab 5’s entity.h writes the typedef as int (*Power)(); rather than int (*Power)(void);. The first form (K&R) and the second (C99) mean the same thing in this context; the lab keeps the older form for consistency with the rest of the course’s source files.

The same pattern shows up in production C. The Linux kernel’s struct file_operations is the textbook example:

struct file_operations {
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int     (*open) (struct inode *, struct file *);
    int     (*release)(struct inode *, struct file *);
    // ... around thirty more
};

Every file system (ext4, NTFS, procfs) fills in its own file_operations struct with its own implementations. When a process calls read() on a file descriptor, the kernel looks up the file’s file_operations, reads the read field, and calls through it. Different file systems, one calling convention, no inheritance.

GUI toolkits use the same shape for click handlers on widget structs. Plug-in hosts use it for hook tables that every plug-in fills in. Lab 5 is the small version of how production C does runtime dispatch.

Self-check: write the declaration

Before continuing to the lab, write (without looking at the Calculator code) the declaration of a struct Job with two fields:

  • int priority
  • run, a function pointer to a function taking struct Job * and returning void

Use a typedef so the function-pointer type has a one-word name.

Show one valid answer
typedef void (*Runner)(struct Job *);

struct Job {
    int priority;
    Runner run;
};

The order of typedef and struct does not matter, as long as Runner is in scope by the time you use it.


What Comes Next

Function pointers let you choose at runtime which function gets called. But qsort uses void* (a typeless pointer) for its parameters. Next, you will learn how void pointers work and why they are C’s mechanism for generic programming.

Next: Void Pointers & Generics