Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Parameter Passing with Pointers
Pass pointers to functions when you need optional parameters or C API compatibility.
Pass by Address
In prior lessons, we've covered two ways to pass arguments to functions: pass by value and pass by reference.
Here's a sample program showing a std::string object being passed by value and by reference:
#include <iostream>
#include <string>
void showByValue(std::string name) // Function parameter is a copy of playerName
{
std::cout << name << '\n';
}
void showByReference(const std::string& name) // Function parameter is a reference that binds to playerName
{
std::cout << name << '\n';
}
int main()
{
std::string playerName{"Artemis"};
showByValue(playerName); // Pass by value, makes a copy
showByReference(playerName); // Pass by reference, no copy
return 0;
}
When we pass argument playerName by value, function parameter name receives a copy. Any changes to name affect only the copy, not the original.
When we pass argument playerName by reference, reference parameter name binds to the actual argument. This avoids making a copy. Because our reference is const, we can't modify name. If name were non-const, changes would affect playerName.
In both cases, the caller provides the actual object (playerName) to the function call.
Pass by Address
C++ provides a third way to pass values to functions, called pass by address. With pass by address, instead of providing an object as an argument, the caller provides an object's address (via a pointer). This pointer is copied into a pointer parameter of the called function. The function can then dereference that pointer to access the object whose address was passed.
Here's a version adding a pass by address variant:
#include <iostream>
#include <string>
void showByValue(std::string name)
{
std::cout << name << '\n';
}
void showByReference(const std::string& name)
{
std::cout << name << '\n';
}
void showByAddress(const std::string* name) // Pointer parameter holds playerName's address
{
std::cout << *name << '\n'; // Print via dereferenced pointer
}
int main()
{
std::string playerName{"Artemis"};
showByValue(playerName); // Pass by value
showByReference(playerName); // Pass by reference
showByAddress(&playerName); // Pass by address
return 0;
}
Let's explore the pass by address version in detail.
First, because we want showByAddress() to use pass by address, we made its parameter a pointer named name. Since showByAddress() uses name read-only, name is a pointer to a const value.
void showByAddress(const std::string* name)
{
std::cout << *name << '\n';
}
Inside the function, we dereference name to access the value being pointed to.
Second, when calling the function, we pass the object's address using the address-of operator (&):
showByAddress(&playerName);
When this executes, &playerName creates a pointer holding playerName's address. This address is copied into function parameter name. Because name now holds playerName's address, dereferencing name gives us playerName's value.
If we already had a pointer variable holding the address, we could use that instead:
int main()
{
std::string playerName{"Artemis"};
showByAddress(&playerName);
std::string* ptr{&playerName}; // Pointer variable holding playerName's address
showByAddress(ptr); // Pass by address using pointer variable
return 0;
}
Nomenclature
When we pass a variable's address as an argument using operator&, we say the variable is passed by address.
When we have a pointer variable holding an object's address, and we pass the pointer as an argument, we say the object is passed by address, and the pointer is passed by value.
Pass by Address Does Not Make a Copy of the Object Being Pointed To
Consider:
std::string playerName{"Artemis"};
showByAddress(&playerName);
As noted in the pass by lvalue reference lesson, copying a std::string is expensive. When passing a std::string by address, we're not copying the actual std::string object—we're just copying the pointer (holding the object's address). Since addresses are typically only 4 or 8 bytes, copying a pointer is always fast.
Thus, like pass by reference, pass by address is fast and avoids making a copy of the argument object.
Pass by Address Allows Modifying the Argument's Value
When we pass an object by address, the function receives the address of the passed object. Because this is the address of the actual argument (not a copy), if the function parameter is a pointer to non-const, the function can modify the argument via the pointer:
#include <iostream>
void applyDamage(int* health)
{
*health -= 25;
}
int main()
{
int playerHealth{100};
std::cout << "Health = " << playerHealth << '\n';
applyDamage(&playerHealth);
std::cout << "Health = " << playerHealth << '\n';
return 0;
}
Output:
Health = 100
Health = 75
The argument is modified and this modification persists after applyDamage() finishes.
If a function shouldn't modify the object, make the parameter a pointer-to-const:
void applyDamage(const int* health)
{
*health -= 25; // Error: can't modify const value
}
Best Practice
Prefer pointer-to-const function parameters over pointer-to-non-const parameters, unless the function needs to modify the object passed in. Do not make function parameters const pointers unless there's a specific reason to do so.
Null Checking
Consider this program:
#include <iostream>
void showHealth(int* hp)
{
std::cout << *hp << '\n';
}
int main()
{
int playerHealth{100};
showHealth(&playerHealth);
int* nullPtr{};
showHealth(nullPtr);
return 0;
}
This program will print 100 and then most likely crash.
In the call to showHealth(nullPtr), nullPtr is a null pointer, so the function parameter will also be null. Dereferencing a null pointer causes undefined behavior.
When passing by address, ensure the pointer isn't null before dereferencing. One way is using a conditional:
#include <iostream>
void showHealth(int* hp)
{
if (hp)
{
std::cout << *hp << '\n';
}
}
int main()
{
int playerHealth{100};
showHealth(&playerHealth);
showHealth(nullptr);
return 0;
}
In more complex functions, it's often more effective to test if the parameter is null as a precondition and handle the negative case immediately:
#include <iostream>
void showHealth(int* hp)
{
if (!hp) // If null pointer, early return
return;
// If we reached here, hp is valid
std::cout << *hp << '\n';
}
int main()
{
int playerHealth{100};
showHealth(&playerHealth);
showHealth(nullptr);
return 0;
}
If a null pointer should never be passed, use an assert:
#include <iostream>
#include <cassert>
void showHealth(const int* hp)
{
assert(hp); // Fail in debug mode if null pointer passed
if (!hp)
return;
std::cout << *hp << '\n';
}
int main()
{
int playerHealth{100};
showHealth(&playerHealth);
showHealth(nullptr);
return 0;
}
Prefer Pass by (Const) Reference
The showHealth() function above doesn't handle null values well—it effectively aborts. Given this, why allow a user to pass in a null value at all? Pass by reference has the same benefits as pass by address without the risk of inadvertently dereferencing a null pointer.
Pass by const reference has other advantages over pass by address.
First, because an object being passed by address must have an address, only lvalues can be passed by address (rvalues don't have addresses). Pass by const reference is more flexible, accepting both lvalues and rvalues:
#include <iostream>
void displayByValue(int val)
{
std::cout << val << '\n';
}
void displayByReference(const int& val)
{
std::cout << val << '\n';
}
void displayByAddress(const int* val)
{
std::cout << *val << '\n';
}
int main()
{
displayByValue(42); // Valid (but makes a copy)
displayByReference(42); // Valid (const reference)
displayByAddress(&42); // Error: can't take address of rvalue
return 0;
}
Second, the syntax for pass by reference is natural—we can just pass literals or objects. With pass by address, code ends up littered with ampersands (&) and asterisks (*).
In modern C++, most things done with pass by address are better accomplished through other methods. Follow this common maxim: "Pass by reference when you can, pass by address when you must".
Best Practice
Prefer pass by reference to pass by address unless you have a specific reason to use pass by address.
Parameter Passing with Pointers - Quiz
Test your understanding of the lesson.
Practice Exercises
Pass by Address
Learn pass by address as an alternative to pass by reference. Understand when to use pointers versus references, null checking, and why pass by reference is usually preferred.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!