AlgoMaster Logo

Pointers and Functions

High Priority35 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A function that takes or returns a pointer can read and write data owned by its caller, share large objects without copying, or even decide at runtime which other function to call. This chapter covers four pointer-and-function patterns: passing a pointer as a parameter, returning a pointer from a function, taking a pointer to a function itself, and choosing between pointer parameters and reference parameters.

Passing a Pointer to a Function

A function parameter declared as T* receives an address. Inside the function, the parameter is a local pointer variable that holds whatever address the caller passed in. Reading or writing through that parameter reaches the caller's object directly, with no copy.

A small e-commerce example: a function that applies a discount to a product in place.

Output:

applyDiscount takes a Product*. The call passes &mouse, the address of the local mouse variable in main. Inside the function, item is a separate pointer variable, but it holds the same address as &mouse, so item->price = ... writes through to the same object that main named mouse. The change is visible after the call returns.

Three details matter when reading this code:

  • The pointer parameter item is a copy. Reassigning item itself (item = nullptr; inside the function) does not affect the caller's pointer.
  • The object the pointer points at is shared. Writing through item (item->price = ...) does affect the caller's object.
  • A null check at the top is a common defensive pattern, because pointer parameters can carry nullptr.

The same shape applies to a non-class pointer. A function that swaps two stock counts:

Output:

swapStock takes two int*. The dereferences *a and *b reach the caller's variables, so the swap modifies them directly. Without pointers (or references), a function could not change mouseStock and keyboardStock from outside.

Quick Check: What does this code print?

<details> <summary>Answer</summary>

valid. Inside reset, p is a copy of ptr. Setting p = nullptr only changes the local copy. The caller's ptr still holds the address of stock. To clear the caller's pointer, the function would need a int** parameter or an int*& reference parameter.

</details>

Returning a Pointer from a Function

A function can also return a pointer. The contract for the caller is "I'll give you back an address; here's what it points at and how long it stays valid". That second half is where most return-pointer bugs come from.

Two situations make returning a pointer safe.

The first is returning the address of something that already exists outside the function. A search function that returns a pointer into a vector the caller owns:

Output:

findByName returns the address of an element inside catalog. The catalog is owned by main, so the element lives until main ends. The returned pointer is valid for that entire scope. If the catalog instead disappeared before the pointer was used, the same code would be broken.

Returning nullptr for "not found" is the convention. The caller checks against nullptr before dereferencing.

The second safe case is returning a pointer to memory allocated with new inside the function. The caller then owns the lifetime and must delete it later:

Output:

This works, but it puts the lifetime burden on every caller. Forget the delete and the program leaks memory. Throw an exception between the call and the delete and the program also leaks. Smart pointers exist to fix this problem, which is why modern code returns std::unique_ptr<Product> instead.

The Dangerous Case: Returning a Pointer to a Local

Returning the address of a function-local variable is a classic C++ bug. The local goes out of scope when the function returns, but the pointer the caller now holds still points at that memory. Dereferencing it is undefined behavior.

What's wrong with this code?

stock is a local variable. Its storage sits on the stack inside makeStock's frame. The moment makeStock returns, that frame is gone, and the memory is free to be reused by the next function call. The returned pointer p is dangling: it holds an address that no longer belongs to a live int.

On some compilers, with no optimization, the program might print 42 because the stack memory hasn't been overwritten yet. On others, or with optimization, it prints garbage or crashes. None of these outcomes are guaranteed; the behavior is undefined.

The compiler often catches this and prints a warning like:

A warning is not an error. The program still compiles. Treating warnings as errors with -Werror is a useful habit for exactly this reason.

The fix depends on what the caller actually needs:

Option 1 is the usual choice for small types. Option 2 fits when the caller wants the function to populate an existing buffer. Option 3 is where modern code uses smart pointers.

The same trap appears with local arrays:

What's wrong with this code?

quantities is a local array. The array name decays into a pointer to its first element, and that element lives on the stack. Returning the pointer hands the caller a dangling address. The fix is to return a std::vector<int> by value, which carries its storage with it.

The vector owns heap-allocated memory and copies (or moves) that ownership into the caller. No dangling, no manual delete.

Pointers to Functions

A function is a piece of code that lives at some address in memory. A pointer can hold that address, the same way a pointer can hold the address of a variable. The pointer's type encodes the function's signature: return type and parameter types.

A small example. Two pricing strategies, one pointer that picks between them:

Output:

pricer is declared as a pointer to a function taking a double and returning a double. The first assignment makes it point at regularPrice. The second reassigns it to salePrice. The call site pricer(100.0) runs whichever function the pointer currently holds.

The declaration syntax needs care. The parentheses around *pricer are required:

Without the inner parentheses, the declaration parses as a function prototype, not a pointer. The reason: function-call () binds tighter than dereference *, so *pricer(double) reads as "call pricer, then dereference the result". Wrapping (*pricer) flips that grouping.

A using alias makes the type readable:

The basic syntax above is enough for this chapter.

Quick Check: Which of these correctly declares a pointer to a function that takes two ints and returns a bool?

  • A) bool* compare(int, int);
  • B) bool (*compare)(int, int);
  • C) bool compare(*int, *int);

<details> <summary>Answer</summary>

B. Option A declares a function that returns bool*. Option C is not valid C++ syntax. Only B declares compare as a pointer to a function, because the parentheses group *compare together.

</details>

Pointer Parameters vs Reference Parameters

C++ has two ways to give a function access to the caller's object without copying it: a pointer parameter, or a reference parameter. They overlap heavily, and choosing between them is mostly a style decision once the constraints are understood.

The same function, twice:

Output:

Both functions do the same job. The pointer version takes Product*, requires the caller to write &a, uses -> to access members, and needs a null check. The reference version takes Product&, lets the caller write b directly, uses . for members, and has no null check because references cannot be null.

The differences come down to four properties:

PropertyPointer parameterReference parameter
Can be nullYesNo
Caller syntaxf(&x) (explicit address)f(x) (looks like by-value)
Member accessp->fieldr.field
Can be reassignedYes (p = &y)No, bound at initialization

Common rules:

  • Use a reference when the parameter must always refer to a valid object. The function says "give me an object, you can't say none".
  • Use a pointer when "no object" is a meaningful possibility, or when the function may switch which object it works with mid-stream, or when working with C APIs that already use pointers.

For the discount example, a reference fits better. There is no sensible meaning for "apply a discount to no product". A pointer would force every caller to write & at the call site for no benefit, and every implementation to add a null check that has no real failure case.

A reasonable pointer parameter, on the other hand:

Here, "no product" is a real state the function is designed to handle. A reference parameter would lie about the contract.

A second, more practical separator: caller-side readability. With a pointer parameter, the call site shows & explicitly:

That & is a visible hint that the function might modify mouse. With a non-const reference parameter, the call looks identical to pass-by-value:

Some style guides (notably the Google C++ Style Guide, historically) require non-const reference parameters be passed as pointers instead, so the call site always shows when an argument might be modified. Other style guides (and modern Google) accept non-const references freely. There is no consensus; pick a convention and stay consistent within a codebase.

For read-only parameters, const T& is the common form for non-trivial types. It avoids the copy of by-value and still cannot be null. A pointer version (const T*) only makes sense when "no object" is a real state.

Quick Check: A function needs to look up a product in a catalog and return the result. The result might be "not found". Should the return type be Product* or Product&?

<details> <summary>Answer</summary>

Product*. A reference cannot represent "not found", because references have to be bound to a real object at all times. Returning Product* lets the function return nullptr when the lookup fails. Modern alternatives include std::optional<Product> (a value type) or Product* (a non-owning pointer). A reference does not fit here.

</details>

A Worked Example: Updating a Cart Through Functions

Putting the pieces together: a small program that operates on a shopping cart through pointer parameters, with both modifying and read-only access.

Output:

Three functions, three pointer patterns:

  • addItem takes a non-const std::vector<CartItem>* and modifies the cart by appending to it.
  • totalPrice takes a const std::vector<CartItem>* and only reads the cart. The const says "I won't modify what this points at".
  • findItem takes a non-const cart pointer and returns a CartItem* into the cart. The caller can modify the found item through the returned pointer, as the cable->quantity = 3 line does.

The findItem return is a case where returning a raw pointer is justified: the cart owns the items, the pointer is non-owning, and the lifetime is obvious from the structure of the code. If the cart were destroyed or resized between the call and the use of the pointer, the pointer would become dangling. That risk is why this pattern often gets replaced by an index or an iterator in larger codebases.

Every function checks for nullptr at the top. That check is appropriate here because the parameter type is T*, which can carry null. If these functions used T& references instead, the checks would not exist, and the caller could not pass "no cart".

Interview Questions

Q1: What happens when a function returns the address of a local variable, and why?

The local variable lives on the stack and is destroyed when the function returns. The returned pointer holds an address that no longer belongs to a valid object, so it is dangling. Dereferencing it is undefined behavior. The program might appear to print the right value if the stack memory has not been overwritten yet, but the behavior is not guaranteed and often breaks under optimization. The fix is to return by value, have the caller pass in a buffer, or allocate on the heap and document the ownership transfer.

**Q2: When should a function parameter be T* instead of T&?**

Use T* when "no object" is a meaningful state the function must handle, when the function may rebind to a different object during its execution, or when interoperating with C APIs that already use pointers. Use T& (or const T&) when the parameter must always refer to a real object. References communicate "always valid" through the type system, while pointers leave that as a runtime check.

Q3: What is the difference between modifying the pointer parameter inside a function versus modifying what it points at?

Modifying the parameter itself (p = nullptr; or p = &other;) only changes the local copy of the pointer. The caller's pointer variable is not affected. Modifying what the parameter points at (*p = 5; or p->field = 5;) does affect the caller's object, because the parameter and the caller's pointer hold the same address and refer to the same memory. To let a function reassign the caller's pointer, the parameter has to be T** or T*&.

Q4: Why do function pointer declarations need parentheses around the name?

The function-call operator () has higher precedence than the dereference *. Without the inner parentheses, int* f(int) reads as "function f that takes an int and returns int*", which is a regular function declaration, not a pointer. Wrapping (*f) forces the parser to group the dereference with the name, producing "pointer to function" instead. The using alias syntax sidesteps the syntactic noise by hiding it inside a type alias.

Q5: What is the risk of returning a non-owning raw pointer into a container, and how is it usually mitigated?

The pointer is only valid while the container element it points at stays put. If the container is destroyed, the element is removed, or the container reallocates (which std::vector does on growth), the pointer becomes dangling. Mitigations include returning an index instead of a pointer, returning an iterator and documenting invalidation rules, restricting the pointer's lifetime to a scope where the container cannot change, or wrapping the result in std::optional for value semantics. For owning return values, std::unique_ptr or std::shared_ptr is the modern choice.

Exercises

Exercise 1: Write a function increment(int* p) that adds 1 to the value pointed to. Call it in main to bump a counter from 0 to 3.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 2: What does this code print?

Expected Output:

<details> <summary>Solution</summary>

The parameter p is a local copy of ptr. Inside rebind, p = &local only changes the copy; ptr in main still points at x. The assignment *p = 50 writes to local, not to x. So x is still 10 when main prints it.

</details>

Exercise 3: Fix the bug in this function.

Expected Output: A working function that gives the caller a value of 100 without dangling-pointer behavior.

<details> <summary>Solution</summary>

The original returns the address of a stack-local variable, which is destroyed on return. For a small built-in type, returning by value is the right fix. The heap version with new int{100} also works, but leaves the caller responsible for delete.

</details>

Exercise 4: Write a function findMax(const int* values, int count) that returns a pointer to the largest element in an array, or nullptr if count is 0.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 5: Predict the output.

Expected Output:

<details> <summary>Solution</summary>

modify takes a pointer to a pointer. Writing *pp = &newValue changes the pointer that pp points at, which is ptr in main. After the call, ptr no longer holds the address of original; it holds the address of newValue, whose value is 77. Because newValue is static, its storage outlives the function call, so ptr is not dangling.

</details>

Exercise 6: Rewrite the following pointer-based function using a reference parameter instead.

<details> <summary>Solution</summary>

The reference version cannot be null, so the null check is unnecessary. The caller writes double_quantity(stock) instead of double_quantity(&stock). A reference fits when "no quantity" is not a meaningful state.

</details>

Exercise 7: Write a function pick(int choice, double base) that returns the result of either regularPrice(base) or salePrice(base) based on choice (0 for regular, 1 for sale). Use an array of function pointers to dispatch.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 8: What is wrong with this function, and how would you fix it?

<details> <summary>Solution</summary>

The array top is local to the function and is destroyed when the function returns. The returned pointer is dangling. The fix is to return a std::vector<int> or std::array<int, 3> by value:

std::array is a fixed-size container that knows its size at compile time, so the size is part of the return type. The value is copied (or moved) to the caller, with no dangling pointer concern.

</details>

Exercise 9: Write a function swapPointers(int** a, int** b) that swaps the two int* values its parameters point at. Demonstrate that after calling it, two pointers in main have traded which variable each points at.

Expected Output:

<details> <summary>Solution</summary>

swapPointers takes pointers to pointers, so it can rewrite the caller's pointer variables. After the swap, p1 holds the address of y and p2 holds the address of x.

</details>

Exercise 10: A team is debating whether update_price(Product*, double) should take its first argument by pointer or by reference. List two reasons to pick a reference and one reason to pick a pointer.

<details> <summary>Solution</summary>

Reasons to prefer a reference:

  1. A product is always required for the update to make sense, so "no object" is not a state the function should handle. A reference enforces this at the type level.
  2. The call site reads more cleanly: update_price(mouse, 19.99) instead of update_price(&mouse, 19.99). No null check is needed inside the function.

Reason to prefer a pointer:

  1. If the project's style guide requires non-const reference parameters to be passed as pointers (so the & at the call site signals that the argument may be modified), a pointer fits. This is a readability convention, not a correctness one.

</details>

Quiz

Pointers & Functions Quiz

10 quizzes