C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
48: Function Pointers
I want to show you something that tripped me up for a good hour early in my career. You're trying to build a flexible system—maybe a plugin architecture or a simple callback—and you decide to use function pointers. You write what looks like perfectly reasonable C code, but the compiler starts screaming at you about types, or worse, it compiles but crashes the moment you try to execute the call.
// The Goal: A function that filters an array based on a custom rule
void filter_array(int *arr, int size, int *predicate(int)) {
for (int i = 0; i < size; i++) {
if (predicate(arr[i])) {
printf("%d passed the test!\n", arr[i]);
}
}
}
int is_even(int n) {
return n % 2 == 0;
}
int main() {
int my_nums[] = {1, 2, 3, 4, 5};
filter_array(my_nums, 5, is_even);
return 0;
}
The Return Type Confusion
If you look closely at the filter_array signature, you'll see the mistake. I wrote int *predicate(int). To a human, that looks like "a predicate function that takes an int and returns an int." But to the C compiler, the function parentheses () have higher precedence than the pointer asterisk *.
C sees this as a function declaration that returns a pointer to an int (int*), not a pointer to a function. When you pass is_even (which returns a plain int) into that slot, you've created a type mismatch. Depending on your compiler settings, you might get a warning, or the program might try to treat the return value of is_even as a memory address, leading to a segmentation fault when the code tries to dereference it.
Grouping the Pointer with Parentheses
To fix this, we have to force the compiler to associate the * with the name of the variable, not the return type. We do this by wrapping the pointer and the name in parentheses.
// The Fix: Notice the (*predicate)
void filter_array(int *arr, int size, int (*predicate)(int)) {
for (int i = 0; i < size; i++) {
if (predicate(arr[i])) {
printf("%d passed the test!\n", arr[i]);
}
}
}
By writing int (*predicate)(int), you're explicitly telling C: "predicate is a pointer to a function that takes an int and returns an int." It's a clunky syntax, I'll admit, but it's the only way to distinguish between a function that returns a pointer and a pointer to a function.
Using Typedefs to Keep Your Sanity
Once you start using function pointers in more than one place, the syntax becomes a nightmare. If you have five different functions taking the same predicate, you don't want to write int (*predicate)(int) every single time. It's noisy and prone to typos.
The professional way to handle this is with a typedef. You can essentially create a new type name for your function signature. I do this in almost every project that uses callbacks.
typedef int (*PredicateFunc)(int);
// Now the signature is clean and readable
void filter_array(int *arr, int size, PredicateFunc predicate) {
// ... implementation stays the same ...
}
Now, PredicateFunc is a first-class type. If you ever need to change the predicate to take a float instead of an int, you only have to change it in one line (the typedef) rather than hunting through every function signature in your codebase.
When to Actually Use This
You might be wondering why we bother with this complexity. Function pointers are the key to implementing "strategies" or "plugins" in C. Imagine you're writing a library for a GUI. You don't know what happens when a user clicks a button—that's decided by the person using your library. You provide a way for them to "register" a function pointer, and your library calls that pointer whenever the click event occurs.
It allows your code to remain generic while the user of your code provides the specific behavior. It's the closest C gets to the "polymorphism" you see in languages like Java or C++.
📋 Practical Task
Building a Mathematical Operation Dispatch Table
Using a giant switch statement to handle different operations is common, but it's not very scalable. Your task is to replace a switch-based calculator with a Dispatch Table (an array of function pointers).
Requirements:
- Create four functions:
add,subtract,multiply, anddivide. Each must take twodoublearguments and return adouble. - Define a
typedeffor a function pointer that matches this signature. - Create an array of these function pointers. The order should be: add, subtract, multiply, divide.
- Write a function
calculate(int op_index, double a, double b)that uses the array to call the correct operation based on the index. - In
main, test thecalculatefunction by passing an index (0-3) and two numbers. Ensure you handle the case where the index is out of bounds of the array to prevent a crash.
There are no comments for now.