Hello! Welcome to your fifth lesson in the C programming module.
In our last lesson, you learned how to create custom data types using structs, which allowed us to group related data into a single, logical unit like a GameObject. We ended with a question: How can we give these data structures behavior? How can a Player object know how to update itself, or an Enemy know how to render?
Today, we'll answer that question by exploring one of C's most powerful and flexible features. Our learning outcome is to define and use function pointers to implement callbacks and flexible systems. A function pointer is a variable that, instead of holding data, holds the memory address of a function. This allows us to treat functions like data—we can store them, pass them to other functions, and call them dynamically. This is the key to creating the modular and extensible systems required for a game engine.
What is a Function Pointer?
Just as a variable has a memory address, a compiled function also resides at a specific address in memory. A function pointer is simply a special type of pointer that stores this address. This allows you to call a function indirectly through the pointer, rather than directly by its name.
The syntax for declaring a function pointer can seem unusual at first, but it mirrors the function it's meant to point to.

Let's look at the syntax: return_type (*pointer_name)(parameter_types);
return_type: The return type of the function this pointer can hold.(*pointer_name): The name of your pointer variable. The parentheses are crucial. Without them,int *my_func(void);would declare a function namedmy_functhat returns a pointer to anint, not a pointer to a function.(parameter_types): The list of parameter types for the function.
The function pointer's signature (return type and parameter types) must exactly match the signature of any function it points to.
To see this in practice, let's watch a short video that covers the basics of declaring a function pointer, assigning a function's address to it, and calling the function through the pointer.
Function Pointers | C Programming Tutorial
This video from Portfolio Courses clearly demonstrates the fundamental mechanics of function pointers.
Watch the beginning of the video, from 00:00 to 02:24. It shows how to declare a pointer to a simple function, assign the function's address, and then call it.
As the video shows, you can assign the address of a function to a pointer. C provides some syntactic sugar here:
void my_function() { /* ... */ }
// Declaration
void (*func_ptr)();
// Both of these assignments are valid and equivalent
func_ptr = &my_function; // Explicitly take the address
func_ptr = my_function; // The function name decays to a pointer
// Both of these calls are valid and equivalent
(*func_ptr)(); // Explicitly dereference the pointer to get the function
func_ptr(); // The compiler understands you mean to call the function
For consistency and clarity, we'll generally use the simpler forms (func_ptr = my_function; and func_ptr();).
The Power of Callbacks
The most common and powerful use of function pointers is to implement callbacks. A callback is a function that is passed as an argument to another function. The receiving function can then "call back" the function it was given at an appropriate time.
This allows you to write generic functions that can be customized at runtime, making your code incredibly flexible. A classic example is a sorting algorithm.
Imagine you've written a function to sort an array of integers in ascending order. What if you also need to sort it in descending order? Or what if you want to sort it by the absolute value of the numbers? You could copy and paste the sorting function and change the comparison logic, but that leads to a lot of duplicate code.
A better solution is to let the sorting function accept a comparison function as an argument. The sorting algorithm remains the same; only the comparison logic, provided by the callback, changes.
This next video brilliantly illustrates this exact scenario. It starts by building a simple Bubble Sort and then refactors it to use a callback function for comparison, making it far more versatile.
Function pointers and callbacks
The mycodeschool channel provides a fantastic, step-by-step explanation of why callbacks are so useful, using a sorting algorithm as the example.
Watch from 02:54 to 11:14. Pay close attention to how the single comparison line if (A[j] > A[j+1]) is replaced by a call to a function pointer if (compare(A[j], A[j+1]) > 0). This is the core idea of a callback.
Real-World Callbacks: The qsort Function
The concept of passing a comparison function isn't just a theoretical exercise; it's used by the C standard library's own generic sorting function, qsort. The qsort function can sort an array of any data type because it delegates the task of comparison to a callback function that you provide.
Its signature is:void qsort(void *base, size_t num_elements, size_t element_size, int (*compare)(const void *, const void *));
void *base: A pointer to the start of the array.void *is used so it can accept an array of any type.size_t num_elements: The number of elements in the array.size_t element_size: The size of a single element (e.g.,sizeof(int)).int (*compare)(...): A function pointer to your comparison callback.
The comparison function must take two const void * pointers (generic pointers to the elements being compared) and return:
- A negative value if the first element should come before the second.
- Zero if they are equal.
- A positive value if the first element should come after the second.
Let's see how this works in practice.
Function pointers and callbacks
Let's continue with the mycodeschool video, as it provides a great explanation of qsort immediately after the Bubble Sort example.
Watch from 11:14 to 14:58. This part explains the qsort function signature and shows how to write a comparison function for it, including the necessary typecasting from void * to int *.
Application in Game Development: Behavior in Structs
Now, let's bring this back to your goal of building a game engine. We can combine structs and function pointers to create something that resembles classes in object-oriented languages like C#. By including function pointers within a struct, you can bundle behavior with data.
Let's upgrade the GameObject struct from our previous lesson.
// Forward-declare the struct so we can use a pointer to it inside the definition.
struct GameObject;
// Define a function pointer type for clarity using 'typedef'
typedef void (*GameObject_Update_Func)(struct GameObject *self, float deltaTime);
typedef void (*GameObject_Render_Func)(struct GameObject *self);
// Our enhanced GameObject struct
typedef struct GameObject {
// Data members
Vector2D position;
int health;
// Behavior members (function pointers)
GameObject_Update_Func update;
GameObject_Render_Func render;
} GameObject;
// --- Example Functions ---
void player_update(GameObject *self, float deltaTime) {
// Logic for updating the player...
// e.g., self->position.x += 50.0f * deltaTime;
printf("Updating player position...\n");
}
void enemy_update(GameObject *self, float deltaTime) {
// Logic for updating the enemy...
printf("Updating enemy AI...\n");
}
void generic_render(GameObject *self) {
// Logic for rendering any object at its position...
printf("Rendering object at (%.1f, %.1f)\n", self->position.x, self->position.y);
}
// --- In main() ---
int main() {
GameObject player;
player.position.x = 100;
player.position.y = 100;
player.update = player_update; // Assign player-specific behavior
player.render = generic_render;
GameObject enemy;
enemy.position.x = 300;
enemy.position.y = 250;
enemy.update = enemy_update; // Assign enemy-specific behavior
enemy.render = generic_render;
// In the main game loop, you can treat them uniformly:
float dt = 0.016f; // example delta time
player.update(&player, dt);
enemy.update(&enemy, dt);
player.render(&player);
enemy.render(&enemy);
return 0;
}
This is a powerful pattern. You can have an array of GameObject* pointers, and in your main game loop, you can iterate through them and call object->update(object, dt) for each one, regardless of whether it's a player, an enemy, or a spinning coin. Each object will execute its own specific update logic. This is a form of polymorphism, achieved in C using structs and function pointers.
Function Pointers in Structures
For a more detailed look at this powerful technique, the article 'Function Pointer in C' from Aticleworld has an excellent section on embedding function pointers in structs.
Read section 9, titled 'Function Pointers in Structures'. It reinforces the concept we just discussed and highlights its benefits for code organization, which is essential for large projects like a game engine.
Important Rules and Pitfalls
Function pointers are different from the data pointers (like int* or char*) you've used so far. Confusing them can lead to crashes. Here are two critical rules:
- You do not allocate memory for function pointers. Function pointers point to code that already exists in your program's executable memory. You should never use
mallocorfreewith them. - Pointer arithmetic does not apply. Operations like
func_ptr++are meaningless for function pointers. They point to the start of a function's instructions, not to an array of data, so incrementing the address will point to invalid code and cause a crash when called.
This final video clearly demonstrates why these operations are invalid and should be avoided.
Function Pointers | C Programming Tutorial
This last clip from the Portfolio Courses video is short but essential. It will help you avoid some of the most common mistakes beginners make with function pointers.
Watch from 15:13 to 18:16. The video explicitly contrasts pointer arithmetic on data pointers with the (invalid) attempt to do so on function pointers.
Practice Exercise
Let's put your new knowledge to the test. You will implement the generic sorting pattern we discussed.
Task:
Write a program that sorts an array of integers in both ascending and descending order using a single sort function and two different callback functions.
- Include
<stdio.h>and<stdlib.h>. - Write the comparison callbacks:
int compare_asc(const void *a, const void *b): This function should take twoconst void *pointers. Inside, cast them toint *, dereference them, and return a value appropriate forqsortto sort in ascending order (e.g.,*(int*)a - *(int*)b).int compare_desc(const void *a, const void *b): Does the same, but returns a value for descending order.
- In your
mainfunction:- Create an array of integers:
int numbers[] = {54, 23, 8, 91, 45, 77, 32}; - Calculate the number of elements in the array.
- First Sort: Call the standard library
qsortfunction, passing it your array and a pointer to yourcompare_ascfunction. - Print the array to verify it's sorted in ascending order.
- Second Sort: Call
qsortagain on the same array, but this time pass a pointer to yourcompare_descfunction. - Print the array again to verify it's now sorted in descending order.
- Create an array of integers:
This exercise will solidify your understanding of how a single generic function can have its behavior modified by passing different callback functions.
Conclusion
You've just unlocked a new level of flexibility in C. Function pointers are the key to decoupling what a function does from how it does it. This is a cornerstone of advanced C programming and is fundamental to building extensible systems.
Key Takeaways:
- A function pointer is a variable that stores the address of a function.
- The syntax is
return_type (*pointer_name)(parameter_types);. - They enable callbacks: functions passed as arguments to other functions.
- Callbacks make code more flexible and reusable, allowing you to write generic algorithms (like
qsort) that can be customized at runtime. - Embedding function pointers in structs allows you to associate behavior with data, a powerful pattern for game development.
- Remember that function pointers are not data pointers: do not use
mallocor pointer arithmetic with them.
Our programs are growing in complexity. We now have custom data types and flexible functions to operate on them. In the next lesson, we'll address the organizational challenges that come with this complexity. You'll learn how to organize C code into multiple source and header files, a crucial skill for managing any project larger than a single file.