5.1.13 · D4C Programming

Exercises — Function pointers — declaration, calling, use in callbacks

2,529 words11 min readBack to topic

Level 1 — Recognition

Exercise 1.1

Which of these declares a pointer to a function taking two ints and returning int?

  • (A) int *p(int, int);
  • (B) int (*p)(int, int);
  • (C) int (p*)(int, int);
  • (D) int *(p)(int, int);
Recall Solution 1.1

The answer is (B).

  • (A) int *p(int,int) — the () after p bind first, so p is a function returning int*. Not a pointer.
  • (C) int (p*)(...)p* is not valid syntax; the * must come before the name.
  • (D) int *(p)(int,int) — the extra parens around p alone do nothing; this is still a function returning int* (same as A).

Only (B) puts * and the name together inside parentheses: (*p). Read inside-out: p is a pointer, (*p)(int,int) is a call, returning int. Star hugs the name, parens make the game.

Exercise 1.2

Given int add(int, int);, mark each as true or false:

  1. add has type int(*)(int,int) when used in p = add;
  2. &add and add produce different addresses.
  3. (*add) is a syntax error.
Recall Solution 1.2
  1. True. A function name decays to its address in most expressions, so add here is an int(*)(int,int) value.
  2. False. &add and the decayed add are the same address — a function has exactly one entry point.
  3. False. *add first decays add to a pointer, then dereferences it back to a function — perfectly legal (it just re-decays again if you try to call it).

Exercise 1.3

What is the type of cmp in qsort's signature? Write it exactly.

Recall Solution 1.3

It is a pointer to a function taking two const void * (see void pointers) and returning int (the sign tells qsort the ordering).


Level 2 — Application

Exercise 2.1

Given

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

Write the code to declare a function pointer p, point it at sub, and call it with (9, 4). Give the result.

Recall Solution 2.1
int (*p)(int, int);   // declare
p = sub;              // sub decays to &sub
int r = p(9, 4);      // call through pointer

sub(9,4) = 9 - 4 = 5. Both p(9,4) and (*p)(9,4) give the same 5.

Exercise 2.2

Complete the comparator so qsort sorts an int array ascending, then predict the output of sorting {5, 2, 8, 1}.

int cmp(const void *a, const void *b) {
    int x = /* ??? */;
    int y = /* ??? */;
    return /* ??? */;
}
Recall Solution 2.2
int cmp(const void *a, const void *b) {
    int x = *(const int *)a;   // reinterpret the void* as int*, then read
    int y = *(const int *)b;
    return (x > y) - (x < y);  // -1, 0, or +1 — safe sign, no overflow
}

Sorting {5,2,8,1} ascending gives =={1, 2, 5, 8}==. Why the cast? qsort doesn't know your element type — it hands you const void * (see void pointers). You know they're int, so you cast then dereference.

Exercise 2.3

Using the same elements {5,2,8,1}, what comparator body produces descending order, and what is the sorted result?

Recall Solution 2.3

Flip the roles of x and y:

return (y > x) - (y < x);   // reverse ordering

Result: =={8, 5, 2, 1}==. Only the returned sign flipped; the qsort call is unchanged.


Level 3 — Analysis

Exercise 3.1

Trace this program and give the printed number.

int add(int a,int b){ return a+b; }
int mul(int a,int b){ return a*b; }
int (*ops[2])(int,int) = { add, mul };
int r = ops[1](ops[0](2,3), 4);
printf("%d\n", r);
Recall Solution 3.1

Inner call first: ops[0](2,3) = add(2,3) = 5. Outer call: ops[1](5, 4) = mul(5,4) = ==20==. The array ops is a dispatch table: ops[0] is add, ops[1] is mul. Indexing selects the behaviour, then (...) calls it. See Arrays in C.

Exercise 3.2

For the comparator return a - b; on 32-bit int, find two values that make it return the wrong sign, and explain why. (Assume int overflow wraps mod .)

Recall Solution 3.2

Take a = 2000000000, b = -2000000000. Mathematically a > b, so a correct comparator returns positive. But a - b = 2000000000 - (-2000000000) = 4000000000, which exceeds INT_MAX = 2147483647. It wraps to a negative result — the wrong sign. qsort now thinks a < b and the sort is corrupted. This is exactly why we use (a>b) - (a<b): it only ever yields -1/0/+1, never overflows.

Exercise 3.3

Why do p(3,4) and (*p)(3,4) compile to the identical call? Explain in terms of decay.

Recall Solution 3.3

A function call needs a function on its left. p is a pointer, so it decays to the function it points at → callable directly: p(3,4). *p dereferences the pointer to get the function itself; but a function immediately decays back to a pointer whenever you try to use it — and a call decays it once more to reach the function. Net effect: both spellings land on the same function with the same call. C deliberately accepts either. (**p)(3,4), (***p)(3,4), … all work too, for the same reason.


Level 4 — Synthesis

Exercise 4.1

Using typedef, define a type BinOp for "pointer to int(int,int)", build a 4-entry dispatch table for add, sub, mul, div_op, and write a one-line call that computes 20 / 5 through the table. Assume the table order is {add, sub, mul, div_op}.

Recall Solution 4.1
typedef int (*BinOp)(int, int);
int add(int a,int b){ return a+b; }
int sub(int a,int b){ return a-b; }
int mul(int a,int b){ return a*b; }
int div_op(int a,int b){ return a/b; }
 
BinOp ops[4] = { add, sub, mul, div_op };
int r = ops[3](20, 5);   // div_op(20,5)

div_op(20,5) = 20 / 5 = ==4==. Index 3 is div_op; the typedef makes BinOp ops[4] read like an ordinary array.

Exercise 4.2

Write a function apply_all that takes an int array, its length, and a BinOp f, and folds the array left-to-right: result = f(f(f(a[0],a[1]),a[2]), …). Then compute the result of applying add to {1,2,3,4} and applying mul to {1,2,3,4}.

Recall Solution 4.2
int apply_all(int *arr, int n, BinOp f) {
    int acc = arr[0];
    for (int i = 1; i < n; i++)
        acc = f(acc, arr[i]);
    return acc;
}
  • add fold: ((1+2)+3)+4 = ==10==.
  • mul fold: ((1*2)*3)*4 = ==24==.

The loop is fixed; the behaviour is passed in as data. This is the essence of a callback and a step toward Polymorphism.

Exercise 4.3

Sort the array {{ "banana", 3 }, { "apple", 1 }, { "cherry", 2 }} (a struct with a char *name and int rank) by rank ascending using qsort. Write the comparator and give the resulting order of names.

Recall Solution 4.3
typedef struct { char *name; int rank; } Item;
 
int by_rank(const void *a, const void *b) {
    const Item *x = a;   // void* implicitly converts to Item*
    const Item *y = b;
    return (x->rank > y->rank) - (x->rank < y->rank);
}
 
qsort(items, 3, sizeof(Item), by_rank);

Ranks are banana=3, apple=1, cherry=2. Ascending by rank → ==apple (1), cherry (2), banana (3)==. qsort handles arbitrary element sizes via the sizeof(Item) argument; your comparator interprets the raw bytes.


Level 5 — Mastery

Exercise 5.1

Is this defined behaviour? Explain precisely.

double half(double x){ return x/2; }
int (*p)(int, int);
p = (int(*)(int,int)) half;   // cast the address
int r = p(10, 4);
Recall Solution 5.1

Undefined behaviour. The cast of the pointer is allowed — converting one function-pointer type to another is legal. What is not allowed is calling through a pointer whose type doesn't match the function's actual type. half expects one double (passed in a floating-point register, typically) and returns a double. Calling it as int(int,int) makes the CPU push two integer arguments and read an integer return — the calling conventions don't line up, so you get garbage or a crash. Rule: you may store/cast a mismatched function pointer, but you must convert it back to the correct type before calling. (Signature mismatch is one of the parent note's four core mistakes.)

Exercise 5.2

What does calling an uninitialized function pointer do, and how does = NULL help if you never assigned it?

int (*p)(int,int);   // uninitialized
if (p) p(1,2);
Recall Solution 5.2

Uninitialized, p holds whatever garbage was on the stack. if (p) is almost always true (garbage is rarely exactly 0), so p(1,2) jumps to a random address → crash or arbitrary code, all undefined behaviour. Initializing int (*p)(int,int) = NULL; makes if (p) reliably false until you assign a real function, so the guard actually protects you. Rule: initialize function pointers to NULL and check before every call you're not certain is assigned.

Exercise 5.3

Predict the exact output.

int add(int a,int b){ return a+b; }
int sub(int a,int b){ return a-b; }
int (*ops[2])(int,int) = { add, sub };
int i = 0;
int r = ops[i++](ops[i](7, 2), 1);
printf("%d\n", r);
Recall Solution 5.3

This is a sequence-point subtlety. The expression reads i (as ops[i]) and modifies it (i++) with no sequence point between them when the two i subexpressions are unsequenced → this is undefined behaviour in C. The compiler may evaluate ops[i] before or after i++. So there is no single correct number — the standard permits any result (or a crash). If it were written safely — e.g. int inner = ops[1](7,2); int r = ops[0](inner, 1); — then:

  • inner: sub(7,2) = 5
  • outer: add(5,1) = ==6==. The lesson: function pointers don't rescue you from the ordinary rules of C evaluation — never read and modify the same object without a sequence point.

Connections