Pointers and Functions

Hi everyone ✋

In the previous post we learned the deep connection between pointers and strings ─ how a C-style string is really just a char array, and how a pointer can walk through it character by character.

If you haven’t read the earlier posts in this series, please go through them first ─ today we’re going to use almost every idea we’ve built so far.

Today’s topic sounds a little strange at first ─ pointers and functions. Wait, can a pointer really point at a function? 🤔

Yes! And once we understand it, we unlock one of the most powerful tricks in all of C++ ─ the ability to treat behavior itself like data, passing functions around just like we pass numbers.

Let’s take a deep dive 🦴

The big idea ─ functions live in memory too

Here’s something we don’t usually think about ─ our functions don’t float in some magic cloud. When our program runs, every function is loaded into memory, and just like a variable, each function sits at a specific address.

And if a function has an address… then we can point a pointer at it 😲

Let’s prove it with a tiny experiment. We’ll print the address of a function.

#include <iostream>
using namespace std;

void greet()
{
    cout << "Hello from greet!" << endl;
}

int main()
{
    // just the function name (no parentheses) gives its address
    cout << "address of greet: " << (void *)greet << endl;

    return 0;
}

The output will be something like ─

address of greet: 0x5591a2b3c1

😲 There it is ─ a real memory address, just like we saw for variables and arrays in the earlier posts.

Notice something familiar here ─ we wrote just greet, without the parentheses (). This is exactly like arrays and strings!

Remember how the array name marks (without an index) gave us the address of the first element?

A function name (without parentheses) does the very same thing ─ it gives us the address of the function 🥳

And just like with a char pointer, we had to cast to void * to actually see the address ─ otherwise cout prints it in a strange way. (Remember the void * trick from last post? Here it is again 😉)

Declaring a pointer to a function

Now that we know a function has an address, let’s store that address in a pointer. But here’s the catch ─ the syntax for a function pointer looks a little scary at first. Don’t worry, we’ll break it down slowly 🦴

To declare a pointer to a function, we have to describe two things ─ what the function returns, and what parameters it takes.

Our greet function returns void and takes no parameters. So a pointer to it looks like this ─

void (*funcPtr)();

Let’s read this carefully, piece by piece ─

  • void ─ the function returns nothing
  • (*funcPtr)funcPtr is a pointer (the * says pointer)
  • () ─ the function takes no parameters

So altogether ─ “funcPtr is a pointer to a function that returns void and takes no parameters.”

Those parentheses around *funcPtr are very important ⚠️ ─ they are not optional decoration. We’ll see exactly why in a moment.

Let’s actually use it.

#include <iostream>
using namespace std;

void greet()
{
    cout << "Hello from greet!" << endl;
}

int main()
{
    // funcPtr is a pointer to a function
    void (*funcPtr)();

    // store the address of greet (no parentheses!)
    funcPtr = greet;

    // now call greet THROUGH the pointer
    funcPtr();

    return 0;
}

The output will be ─

Hello from greet!

🥳🥳 We just called a function through a pointer!

Look at the two key lines.

First, funcPtr = greet; ─ we stored the function’s address in the pointer, using just the name with no parentheses (exactly like int *ptr = marks; from the arrays post).

Then funcPtr(); ─ we called the function through the pointer, by adding the parentheses.

So the pointer became a second name for greet, and calling funcPtr() did the exact same thing as calling greet() directly.

Why those parentheses matter ⚠️

Remember I said the parentheses around *funcPtr were important? Let’s see what happens without them.

void *funcPtr();     // this is NOT a function pointer!
void (*funcPtr)();   // THIS is a function pointer

These two lines look almost identical, but they mean completely different things.

The first one, void *funcPtr();, is read as “a function named funcPtr that takes no parameters and returns a void *.” That’s a function declaration, not a pointer at all! 😱

The second one, void (*funcPtr)();, uses parentheses to force the * to bind to funcPtr first ─ making it “a pointer to a function.”

That single pair of parentheses changes everything. This is one of the trickiest bits of C++ syntax, so whenever you write a function pointer, double-check those parentheses are there 🤔

A cleaner way with dereferencing

Since funcPtr holds the address of a function, and dereferencing a pointer gives us back what it points to, we can also call the function like this ─

(*funcPtr)();   // dereference, then call

This is the “explicit” style ─ we dereference funcPtr to get the function, then call it. Let’s see both styles together.

#include <iostream>
using namespace std;

void greet()
{
    cout << "Hello from greet!" << endl;
}

int main()
{
    void (*funcPtr)() = greet;

    funcPtr();        // short style
    (*funcPtr)();     // explicit dereference style

    return 0;
}

The output will be ─

Hello from greet!
Hello from greet!

Both lines do exactly the same thing 🥳 Modern C++ lets us use the short form funcPtr(), but you’ll often see the explicit (*funcPtr)() in older code. It’s good to recognize both.

Function pointers with parameters and return values

Our greet example was simple ─ no parameters, no return value. Let’s level up to a function that actually takes inputs and returns something.

The rule is the same ─ the pointer’s type must exactly match the function’s return type and parameter types.

#include <iostream>
using namespace std;

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

int main()
{
    // pointer to a function that takes (int, int) and returns int
    int (*operation)(int, int);

    operation = add;

    // call add through the pointer
    cout << "result: " << operation(5, 3) << endl;

    return 0;
}

The output will be ─

result: 8

🥳 Reading the declaration the same way as before ─ “operation is a pointer to a function that takes two ints and returns an int.” And because add matches that shape perfectly, we can point operation at it and call it.

Notice how the type must match precisely. If add took three parameters, or returned a double, this pointer wouldn’t fit it. A function pointer is picky about shape ─ but that pickiness is exactly what makes it safe 🥳

The real power ─ passing functions to functions 🚀

Now here’s where function pointers become genuinely useful, and it connects straight back to our pass-by-reference post.

Just like we can pass a number or an address into a function, we can pass a function into another function. This means we can write a function whose behavior is decided by whatever function we hand it. This is called a callback ─ the passed-in function gets “called back” from inside 🤯

Let’s build a simple example. We’ll write one function calculate that does some math on two numbers ─ but which math is decided by the function pointer we pass in.

#include <iostream>
using namespace std;

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

int multiply(int a, int b)
{
    return a * b;
}

// this function takes TWO numbers AND a function to apply
int calculate(int x, int y, int (*op)(int, int))
{
    return op(x, y);   // call whichever function was passed in
}

int main()
{
    cout << "add:      " << calculate(6, 2, add) << endl;
    cout << "multiply: " << calculate(6, 2, multiply) << endl;

    return 0;
}

The output will be ─

add:      8
multiply: 12

🥳🥳 Same calculate function, but two completely different results ─ because we handed it two different functions!

Let’s understand the magic. Inside calculate, we don’t know or care what op does ─ we just call op(x, y) and trust it. When we passed add, it added. When we passed multiply, it multiplied. The behavior became a parameter we can swap out 🤔

This is incredibly powerful. It’s the idea behind sorting functions that let you choose your own comparison, event handlers that run your custom code, and countless other flexible designs. You write the structure once, and let the caller plug in the behavior.

A practical example ─ applying an operation to a whole array 🥳

Let’s combine today’s lesson with what we learned in the arrays post. We’ll write a function that walks through an array and applies any transformation we choose to each element.

#include <iostream>
using namespace std;

int doubleIt(int x)
{
    return x * 2;
}

int square(int x)
{
    return x * x;
}

// apply a given function to every element of the array
void transform(int *arr, int size, int (*func)(int))
{
    for (int i = 0; i < size; i++)
    {
        arr[i] = func(arr[i]);   // transform each element
    }
}

void printArray(int *arr, int size)
{
    for (int i = 0; i < size; i++)
    {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main()
{
    int numbers[5] = {1, 2, 3, 4, 5};

    transform(numbers, 5, doubleIt);
    cout << "after doubling: ";
    printArray(numbers, 5);

    transform(numbers, 5, square);
    cout << "after squaring: ";
    printArray(numbers, 5);

    return 0;
}

The output will be ─

after doubling: 2 4 6 8 10
after squaring: 4 16 36 64 100

🥳🥳🥳 One transform function, and we reshaped the entire array in two totally different ways just by swapping the function we passed in!

Notice how everything from the series came together here ─ the array decayed into a pointer (int *arr), we walked it with an index, and we passed a function pointer (int (*func)(int)) to decide the behavior. This is the kind of clean, reusable code that pointers make possible 🚀

A friendlier name with typedef

You might be thinking ─ “that int (*func)(int) syntax is hard to read.” You’re right! 😅 When function pointers appear a lot, that ugly syntax gets tiring.

C++ lets us give the type a friendly nickname using typedef (or the modern using). This doesn’t change anything ─ it just makes the code easier to read.

#include <iostream>
using namespace std;

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

// give the messy type a clean name: "Operation"
typedef int (*Operation)(int, int);

int calculate(int x, int y, Operation op)
{
    return op(x, y);
}

int main()
{
    Operation myOp = add;   // much cleaner!

    cout << "result: " << calculate(10, 5, myOp) << endl;

    return 0;
}

The output will be ─

result: 15

🥳 Now Operation means “a pointer to a function taking (int, int) and returning int.” The code reads much more nicely, and we didn’t lose any power ─ it’s the same function pointer underneath, just wearing a friendlier label.

A quick note on modern C++

Function pointers are the classic, foundational way to pass behavior around ─ and understanding them is essential, because they show up everywhere in C and older C++ code. But modern C++ also offers newer tools for the same job, like lambdas and std::function, which are often more flexible.

#include <iostream>
#include <functional>
using namespace std;

int main()
{
    // a lambda ─ a small function written inline
    function<int(int, int)> op = [](int a, int b) {
        return a + b;
    };

    cout << "result: " << op(4, 7) << endl;

    return 0;
}

The output will be ─

result: 11

Don’t worry about fully understanding lambdas yet ─ they deserve their own post 😉 The key thing to know is this ─ they’re built on the very same idea we learned today, treating behavior like data. Once you understand function pointers, lambdas will feel like a natural next step 🥳

So what should we remember? 🤔

Let’s wrap up the key takeaways ─

  • Functions live in memory and have addresses, just like variables and arrays.
  • A function name without parentheses gives its address ─ exactly like an array name gives the address of its first element.
  • A function pointer is declared as returnType (*name)(paramTypes) ─ and the parentheses around *name are mandatory.
  • We call through a function pointer with either funcPtr() or the explicit (*funcPtr)() ─ both do the same thing.
  • The pointer’s type must exactly match the function’s return type and parameters.
  • Passing a function into another function (a callback) lets us decide behavior at runtime ─ the same structure, different logic.
  • typedef (or using) gives the messy function-pointer type a friendly name.
  • Modern C++ builds on this idea with lambdas and std::function, but they all share the same foundation.

Once these click, you’ll start seeing functions not just as blocks of code, but as values you can pass around ─ and that’s a genuinely powerful shift in thinking 👀✨

Congratulations 🥳🥳🥳
We now understand the fascinating relationship between pointers and functions.

In the next post we will talk about dynamic memory allocation ─ how we can use new and delete to create memory on demand while our program runs, which is where pointers truly become essential 😉

Happy Coding 💻 🎵

Leave a Comment

Your email address will not be published. Required fields are marked *