Why this matters
Every pointer so far had a definite type: int *, char *, const char **. Two more kinds of pointer complete the picture, and both are about writing generic code. A void * can hold the address of any object, which is how memcpy, qsort and (next module) malloc work on data of any type. A function pointer holds the address of a function, so you can pass behavior around as a value: tell qsort how to compare, register a callback, or pick an operation from a table. These two ideas are how C does what other languages do with generics, interfaces and lambdas.
void *: an address with no type
void *p;
is a pointer that says nothing about what it points to. The rules:
- Any object pointer converts to
void *and back implicitly, with no cast:int x; void *p = &x; int *q = p;is fine in C. - You cannot dereference a
void *. Without a type, the compiler does not know how many bytes to read:main.c:6:20: warning: dereferencing 'void *' pointer main.c:6:20: error: invalid use of void expression - No arithmetic in standard C:
p + 1has no element size to scale by. (GCC allows it as an extension, treating the size as 1. Do not rely on it.)
So a void * is useful as a carrier: functions accept it so that callers can pass any type, and inside, the function converts it to something concrete. You have already met two such functions: printf("%p", (void *)p) expects a void *, and memset/memcpy take void * parameters, which is why you can hand them an array of any type.
Working on raw bytes
The concrete type a generic function usually converts to is unsigned char *: a pointer to individual bytes, which the C standard explicitly allows for inspecting any object's memory. Here is a swap that works for any type, given the size:
#include <stdio.h>
#include <string.h>
/* Swap two objects of any type, given their size in bytes. */
void swap_any(void *a, void *b, size_t size)
{
unsigned char *x = a;
unsigned char *y = b;
for (size_t i = 0; i < size; i++) {
unsigned char tmp = x[i];
x[i] = y[i];
y[i] = tmp;
}
}
int main(void)
{
int i1 = 1, i2 = 2;
double d1 = 1.5, d2 = -2.25;
char s1[8] = "left", s2[8] = "right";
swap_any(&i1, &i2, sizeof i1);
swap_any(&d1, &d2, sizeof d1);
swap_any(s1, s2, sizeof s1);
printf("%d %d | %g %g | %s %s\n", i1, i2, d1, d2, s1, s2);
return 0;
}
2 1 | -2.25 1.5 | right left
swap_any knows nothing about ints or doubles. It sees two addresses and a byte count, and swaps the bytes. This is precisely how the library's generic functions are built, and it shows the price of genericity in C: the caller must supply the size, and nothing checks that the two pointers really point to objects of that size. Pass the wrong size and you corrupt memory.
Function pointers
Compiled functions are machine code at some address in memory, so they have addresses too. A function pointer stores one.
#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int apply(int (*op)(int, int), int x, int y)
{
return op(x, y);
}
int main(void)
{
int (*f)(int, int) = add;
printf("f(3, 4) = %d\n", f(3, 4));
f = mul;
printf("f(3, 4) = %d\n", f(3, 4));
printf("apply(add, 10, 5) = %d\n", apply(add, 10, 5));
printf("apply(mul, 10, 5) = %d\n", apply(mul, 10, 5));
return 0;
}
f(3, 4) = 7
f(3, 4) = 12
apply(add, 10, 5) = 15
apply(mul, 10, 5) = 50
Reading and writing the syntax
int (*f)(int, int);
Start from the name: f is a pointer (*f, in parentheses) to a function taking (int, int) and returning int. A recipe for writing one: take the function's prototype, int add(int, int), and replace the name with (*f). The parentheses are essential: int *f(int, int); declares a function returning int *.
- Assigning: a function's name decays to its address, just as an array's name does.
f = add;andf = &add;mean the same. - Calling:
f(3, 4)calls the functionfpoints to. The long form(*f)(3, 4)is equivalent; you will see both. - Types must match: assigning a function with a different signature is an error in GCC 14 (
initialization of 'int (*)(int, int)' from incompatible pointer type).
f machine code (read-only, executable)
+------+ +---------------------------+
| *---+------------------>| add: lea eax,[rdi+rsi] |
+------+ | ret |
+---------------------------+
| mul: mov eax,edi |
| imul eax,esi |
| ret |
+---------------------------+
after f = mul, the arrow moves to mul
apply takes a function as a parameter. A function passed to another function so that it can be "called back" is a callback. apply does not know or care which operation it performs; the caller decides.
qsort: the callback you will use most
The C library's sort function is generic over both the element type and the ordering:
void qsort(void *base, size_t count, size_t size,
int (*compar)(const void *, const void *));
You pass the array (as void *), the number of elements, the size of each, and a comparison function. qsort moves elements around as raw bytes (like swap_any) and calls your function whenever it needs to know the order of two elements, passing pointers to them. Your function returns negative if the first should come first, positive if second, 0 if they are equal.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int cmp_int(const void *a, const void *b)
{
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y); /* -1, 0 or 1, and no overflow */
}
int cmp_str(const void *a, const void *b)
{
const char *const *x = a; /* each element is a const char * */
const char *const *y = b;
return strcmp(*x, *y);
}
int main(void)
{
int nums[] = {42, -7, 19, 0, 3};
const char *names[] = {"lisp", "c", "rust", "ada", "go"};
qsort(nums, 5, sizeof nums[0], cmp_int);
qsort(names, 5, sizeof names[0], cmp_str);
for (int i = 0; i < 5; i++) printf("%d ", nums[i]);
printf("\n");
for (int i = 0; i < 5; i++) printf("%s ", names[i]);
printf("\n");
return 0;
}
-7 0 3 19 42
ada c go lisp rust
Two details worth studying:
return x - y;is a classic bug. It looks neat but overflows when, say,xis large andyis very negative, and signed overflow is undefined behavior.(x > y) - (x < y)is overflow-free: each comparison is 0 or 1.- The string comparator receives pointers to the elements, and each element is itself a
const char *. So the arguments are reallyconst char *const *(pointers to const pointers to const chars), and you dereference once to get the strings. Passingastraight tostrcmpwould compare the bytes of the pointers, not the text: a common bug that compiles silently, becausevoid *converts to anything.
Tables of function pointers
An array of function pointers turns a decision into a lookup:
int (*ops[])(int, int) = {add, mul};
printf("%d\n", ops[1](6, 7)); /* calls mul: 42 */
Read int (*ops[])(int, int) from the name: ops is an array ([]) of pointers (*) to functions (int, int) returning int. Paired with a table of names, it becomes a dispatch table: look up a command, call the matching function. Interpreters, command-line tools with subcommands (git commit, git push) and device drivers in the Linux kernel are organized this way. Adding an operation means adding a function and a table entry, not editing a growing chain of if statements. (When you learn typedef and structs in a later module, these declarations get much easier to read.)
Common mistakes
- Dereferencing a
void *without converting it first. - Wrong size with generic functions:
qsort(nums, n, sizeof nums, cmp_int)passes the size of the whole array as the element size. Usesizeof nums[0]. - Comparator that subtracts: overflow for extreme values.
- Comparator with the wrong level of indirection for arrays of pointers: you get pointers to the elements, so dereference once.
- Forgetting the parentheses:
int *f(int)is a function declaration, not a pointer. - Calling a NULL function pointer: segmentation fault. Check before calling if the pointer may be unset.
Recap
void *holds any object's address; it converts implicitly to and from other object pointers but cannot be dereferenced or (in standard C) used in arithmetic.- Generic C functions take
void *plus a size, and work on bytes throughunsigned char *. int (*f)(int, int)is a pointer to a function; assign a function name, call withf(a, b).- Callbacks pass behavior into a function;
qsorttakes a comparison function returning negative, zero or positive. - Arrays of function pointers make dispatch tables.
Going further: read man 3 qsort and man 3 bsearch; bsearch searches a sorted array with the same kind of comparison function and returns a void * to the match or NULL.