Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Advanced Pointer Parameter Techniques
Handle null pointer arguments and modify pointer parameters themselves.
Pass by Address (Part 2)
This lesson continues our coverage of pass by address.
Pass by Address for "Optional" Arguments
One common use for pass by address is allowing a function to accept an "optional" argument:
#include <iostream>
void displayScore(const int* bonus = nullptr)
{
if (bonus)
std::cout << "Your bonus is " << *bonus << " points.\n";
else
std::cout << "No bonus available.\n";
}
int main()
{
displayScore(); // No bonus yet
int earnedBonus{150};
displayScore(&earnedBonus); // Now we have a bonus
return 0;
}
Output:
No bonus available.
Your bonus is 150 points.
The displayScore() function has one parameter passed by address and defaulted to nullptr. In main(), we call this function twice. The first call has no argument, so bonus defaults to nullptr and prints the "no bonus" message. For the second call, we have a valid bonus, so we pass its address and the function prints the bonus value.
However, function overloading is often a better alternative:
#include <iostream>
void displayScore()
{
std::cout << "No bonus available.\n";
}
void displayScore(int bonus)
{
std::cout << "Your bonus is " << bonus << " points.\n";
}
int main()
{
displayScore(); // No bonus yet
int earnedBonus{150};
displayScore(earnedBonus); // Now we have a bonus
displayScore(75); // Also works with rvalue arguments
return 0;
}
This approach has advantages: no null dereference risk, and we can pass literals or rvalues as arguments.
Changing What a Pointer Parameter Points At
When we pass an address to a function, that address is copied from the argument into the pointer parameter. Consider this program:
#include <iostream>
// [[maybe_unused]] removes compiler warnings about param being set but not used
void clearPointer([[maybe_unused]] int* param)
{
param = nullptr; // Make the function parameter null
}
int main()
{
int health{100};
int* ptr{&health}; // ptr points to health
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
clearPointer(ptr);
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
return 0;
}
Output:
ptr is non-null
ptr is non-null
Changing the address held by the pointer parameter had no impact on the argument (ptr still points at health). When clearPointer() is called, param receives a copy of the address passed in. Changing what param points at only affects the copy.
So what if we want to allow a function to change what a pointer argument points to?
Pass by Address... by Reference?
Yes, it's a thing. Just like we can pass a normal variable by reference, we can also pass pointers by reference:
#include <iostream>
void clearPointer(int*& refToPtr) // refToPtr is now a reference to a pointer
{
refToPtr = nullptr;
}
int main()
{
int health{100};
int* ptr{&health}; // ptr points to health
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
clearPointer(ptr);
std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n");
return 0;
}
Output:
ptr is non-null
ptr is null
Because refToPtr is a reference to a pointer, when ptr is passed as an argument, refToPtr is bound to ptr. Any changes to refToPtr are made to ptr.
As an Aside
Because references to pointers are fairly uncommon, it's easy to mix up the syntax (is it int*& or int&*?). If you get it backwards, the compiler will error because you can't have a pointer to a reference (pointers must hold an object's address, and references aren't objects). Then you can switch it around.
Why Using 0 or NULL is No Longer Preferred (Optional)
Literal 0 can be interpreted as either an integer literal or a null pointer literal. In certain cases, this creates ambiguity—the compiler may assume one when we mean the other, with unintended consequences.
The definition of preprocessor macro NULL is not defined by the language standard. It can be defined as 0, 0L, ((void*)0), or something else entirely.
When using function overloading, 0 or NULL can cause problems:
#include <iostream>
#include <cstddef> // for NULL
void handleInput(int value) // Accepts an integer
{
std::cout << "handleInput(int): " << value << '\n';
}
void handleInput(int* ptr) // Accepts an integer pointer
{
std::cout << "handleInput(int*): " << (ptr ? "non-null\n" : "null\n");
}
int main()
{
int health{100};
int* ptr{&health};
handleInput(ptr); // Always calls handleInput(int*) (good)
handleInput(0); // Always calls handleInput(int) (hopefully expected)
handleInput(NULL); // Could do any of these:
// Call handleInput(int) (Visual Studio does this)
// Call handleInput(int*)
// Result in an ambiguous function call error (gcc and Clang do this)
handleInput(nullptr); // Always calls handleInput(int*)
return 0;
}
When passing integer value 0 as a parameter, the compiler prefers handleInput(int) over handleInput(int*). This can lead to unexpected results when we intended handleInput(int*) to be called with a null pointer argument.
Using nullptr removes this ambiguity—it will always call handleInput(int*), since nullptr only matches pointer types.
std::nullptr_t (Optional)
Since nullptr can be differentiated from integer values in function overloads, it must have a different type. nullptr has type std::nullptr_t (defined in header std::nullptr_t can only hold one value: nullptr.
If we want a function that accepts only a nullptr literal argument, we can make the parameter a std::nullptr_t:
#include <iostream>
#include <cstddef> // for std::nullptr_t
void handleInput(std::nullptr_t)
{
std::cout << "in handleInput(std::nullptr_t)\n";
}
void handleInput(int*)
{
std::cout << "in handleInput(int*)\n";
}
int main()
{
handleInput(nullptr); // Calls handleInput(std::nullptr_t)
int health{100};
int* ptr{&health};
handleInput(ptr); // Calls handleInput(int*)
ptr = nullptr;
handleInput(ptr); // Calls handleInput(int*) (ptr has type int*)
return 0;
}
The function call handleInput(nullptr) resolves to handleInput(std::nullptr_t) over handleInput(int*) because it doesn't require a conversion.
The case that might be confusing is calling handleInput(ptr) when ptr holds value nullptr. Function overloading matches on types, not values, and ptr has type int*. Therefore, handleInput(int*) is matched.
There is Only Pass by Value
While the compiler can often optimize references away entirely, there are cases where a reference is actually needed. References are normally implemented using pointers behind the scenes. This means pass by reference is essentially just pass by address.
And pass by address just copies an address from the caller to the called function—which is passing an address by value.
Therefore, we can conclude that C++ really passes everything by value! The properties of pass by address (and reference) come solely from the fact that we can dereference the passed address to change the argument, which we cannot do with a normal value parameter.
Advanced Pointer Parameter Techniques - Quiz
Test your understanding of the lesson.
Practice Exercises
Pass by Address Part 2
Explore advanced pass by address techniques including optional parameters, changing what pointers point to, and passing pointers by reference. Understand why nullptr is preferred over 0 or NULL.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!