Passing a Function Through Another Function in C

 

Introduction

In C-programming, one of the techniques that helps write more flexible and reusable code is passing a function as an argument to another function. In other languages (such as Python or JavaScript) this is natural, but even in C you can do it — using function pointers. In this ProgVeda style tutorial, you’ll understand:

  • What it means to pass a function through another function
  • Why you might want to do it
  • How to declare and use function pointers for that purpose
  • Several concrete examples
  • Some caveats and best-practices

Let’s begin.

 

Why pass a function to another function?

Passing functions as arguments allows you to write higher-order functionality: code that can be configured with different behaviours without rewriting the whole logic. Some benefits:

  • Reusability: You write a general algorithm (say, sort, scan, apply) and you pass in the specific action you want performed.
  • Flexibility: At runtime you can decide which function to use.
  • Separation of concerns: The outer function handles orchestration/control-flow; the passed-in function handles the specific action.
  • Reduce duplication: Instead of writing similar loops for different behaviours, write one loop and pass in the behaviour.

In C, the mechanism is function pointers. While C doesn’t have “first-class functions” like in functional languages, the pointer mechanism gives us enough to achieve similar patterns.

 

Function pointers: the basics

Before passing a function around, you must know how to declare and use a function pointer.

Declaring a function pointer

Suppose you have a function:

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

A pointer to such a function would look like:

int (*fp)(int, int);

Here:

  • int is the return type of the function.
  • (*fp) indicates fp is a pointer to a function.
  • (int, int) are the parameter types.

You can assign:

fp = add;

and call via pointer:

int result = fp(3, 4);  // calls add(3,4)

 

Passing a function pointer to another function

Suppose you write a function that takes a function pointer as argument:

void apply(int (*operation)(int, int), int x, int y) {
int result = operation(x, y);
printf("Result is %d\n", result);
}

Then you can call:

apply(add, 5, 6);  // passes the add function

This is the pattern: you pass a function pointer (operation) into apply, thereby making apply generic/universal for any two-int→int functions.

 

Example: Passing computation behaviour

Let’s build a more complete example to illustrate.

#include <stdio.h>
// Some candidate functions
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
// A function that takes a function pointer
void compute(int (*op)(int, int), int x, int y) {
int res = op(x, y);
printf("Operation result with %d and %d is %d\n", x, y, res);
}
int main(void) {
int a = 10, b = 5;
printf("Using add:\n");
compute(add, a, b); // should print 15
printf("Using multiply:\n");
compute(multiply, a, b); // should print 50
return 0;
}

Explanation

  • We declare two concrete functions: add and multiply.
  • We declare a generic function compute that takes a pointer to a function op of type int (int, int), and two ints x, y.
  • Inside compute, we call op(x, y) without caring which function it is — it could be add, multiply, or some other.
  • In main, we pass add and multiply respectively to compute. This results in different behaviour.
  • The key is the function pointer type signature must match the functions you pass.

What you learn

  • compute is decoupled from the specific operation; it works with any function matching the signature.
  • You easily extend by writing new functions (say subtract, divide) and reuse compute.
  • You avoid repeating the flow (getting inputs, calling operation, printing). You just change what the operation is.

 

Example: Callback style – scanning an array

Let’s look at another pattern: you have an array and you want to apply different functions to each element, and perhaps you pass a function that defines how you “process” each element.

#include <stdio.h>
void apply_to_array(int *arr, int size, void (*func)(int)) {
for (int i = 0; i< size; i++) {
func(arr[i]);
}
}
void print_element(int x) {
printf("%d ", x);
}
void double_and_print(int x) {
printf("%d ", x * 2);
}
int main(void) {
int numbers[] = {1, 3, 5, 7, 9};
int n = sizeof(numbers) / sizeof(numbers[0]);
printf("Original elements: ");
apply_to_array(numbers, n, print_element);
printf("\n");
printf("Doubled elements: ");
apply_to_array(numbers, n, double_and_print);
printf("\n");
return 0;
}

Explanation

  • apply_to_array takes three things: an array arr, its size, and a function pointer func of type void (int) (i.e., takes an int, returns void).
  • Inside it loops through every element of arr, and calls func(arr[i]).
  • Two operations defined: print_element (just prints) and double_and_print (doubles then prints).
  • In main, we call apply_to_array twice passing different functions. Thus the loop logic is reused; behaviour varies by the passed function.

Key take-away

  • The outer function controls the iteration or flow.
  • The inner function (passed) controls the action on each element.
  • This separation is very useful when you may have many possible “actions” but one iteration logic.

 

Some more advanced tips and notes

Here are additional things to keep in mind when using function pointers and passing functions:

  1. Signature must match exactly — return type, parameter types. If you pass a function pointer of the wrong signature, you’ll get compile errors or worse runtime issues.
  2. Const correctness & qualifiers — you can pass pointers to constdata, or use const in parameters if required.
  3. Using typedef — to make code cleaner, you can define a typedef for a function pointer type. Example:
  4. typedef int (*binary_op)(int, int);
  5.  
  6. void compute2(binary_op op, int x, int y) {
  7. printf("Result: %d\n", op(x, y));
  8. }

This improves readability.

  1. Passing pointers to functions that take different numbers of parameters — you can, but you must declare correct pointer type. C doesn’t support generic function pointers (without void-pointer hack) in a safe way.
  2. Inline functions vs function pointers — sometimes you might prefer inline code for performance; function pointers introduce indirection which might have minor overhead.
  3. NULL checking — when you accept a function pointer, you might check if it’s NULL before calling, to avoid segmentation fault.
  4. Callbacks — function pointers are used for callback patterns (e.g., library code calls back user-provided function when something happens).
  5. Passing multiple functions — you can pass more than one function pointer, e.g., void process(int *arr, int size, void (*onSuccess)(int), void (*onError)(int)).
  6. State/context — sometimes you need to pass additional context/state alongside the function pointer (e.g., a void* context pointer). Though in pure C you might simulate closures by passing extra parameter(s).
  7. Function pointer arrays — you can keep an array of function pointers and dynamically choose which to call at runtime.

 

Putting it all together: Example with typedef + callback

Here’s a composite example combining several ideas:

#include <stdio.h>
typedef int (*bin_op)(int, int);
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int max_of_two(int a, int b) {
return (a > b) ?a :b;
}
void process_list(int *arr, int len, bin_op op) {
printf("Applying operation on list: ");
for (int i = 0; i<len - 1; i++) {
int result = op(arr[i], arr[i+1]);
printf("%d ", result);
}
printf("\n");
}
int main(void) {
int data[] = {10, 20, 15, 30, 25};
int n = sizeof(data) / sizeof(data[0]);
printf("With add:\n");
process_list(data, n, add);
printf("With subtract:\n");
process_list(data, n, subtract);
printf("With max_of_two:\n");
process_list(data, n, max_of_two);
return 0;
}

Explanation

  • We define a typedef bin_op for “function taking two ints and returning int”.
  • We define three specific functions matching that signature: add, subtract, max_of_two.
  • process_list takes an array and length, and a bin_op (function pointer) named op. It then applies that operation to each adjacent pair in the array, printing results.
  • In main, we call process_list three times with different operations — showing reuse of the same processing logic with different behaviours.

This demonstrates how passing a function into another function helps you create generic code which is then specialized by the behaviour you pass in.

 

Best practices & pitfalls

Here are some good practices for clarity and safety when passing functions around:

  • Use descriptive names for your function pointer parameters (e.g., operation, callback, handler, fn).
  • Document the expected signature of the function pointer parameter (return type, parameter types).
  • Use typedef for readability when the signature is reused often.
  • Check for NULL pointers if it’s possible the caller passes NULL.
  • Use static functions (file-scope) when the helper functions aren’t needed outside the translation unit.
  • Be aware of code clarity: Over-use of function pointers can make code harder to read for less experienced C programmers — so use when it makes sense.
  • Performance: Indirect calls via pointers may be slightly slower than direct calls — but in most cases this is negligible.
  • Compatibility & portability: Function pointers work in standard C. Avoid overly complex pointer arithmetic with pointers to functions (which is less portable).
  • Avoid mismatched calling conventions: If you’re mixing with libraries or different compilers, ensure calling conventions align (especially in embedded systems).
  • Testing: Since you pass behaviour, test different behaviours (functions) thoroughly via your generic function to ensure correctness.

 

Your next step

Try it out yourself:

  • Write a function that takes an array of floats and a function pointer that processes each float (for example: square, half, double, root).
  • Use typedef to define a pointer type.
  • Try passing different functions to observe different behaviours.
  • Then extend: write a function that takes two arrays and a binary operation, and computes one result array from applying the operation pair-wise.

Once you get comfortable with this, you’ll find yourself naturally structuring code with callbacks and more generic algorithms.

 


Post a Comment

Previous Post Next Post