In prior lessons, we’ve covered pass by value
(2.3 -- Introduction to function parameters and arguments) and pass by reference
(8.5 -- Pass by reference). Pass by value
allows us to pass a copy of an argument to the function. Because the function parameter receives a copy of the argument, any changes made to the parameter are not made to the argument. Pass by reference
allows us to pass a reference to the argument to the function. This avoids making a copy of the argument, and (if the reference is non-const) also allows the function to change the value of the argument.
C++ provides a third way to pass values to a function, called pass by address
. Pass by address involves passing the address of an argument to the function by using a pointer parameter.
Here is an example of a function that takes a parameter passed by address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void print(int* ptr) // The function parameter is a pointer { std::cout << *ptr; // use dereference operator (*) to get the value at the address held by the pointer } int main() { int x{ 5 }; int* ptr { &x }; // ptr holds the address of variable x print(ptr); // we can pass a pointer variable print(&x); // or we can use the address-of operator (&) to get a pointer to a normal variable return 0; } |
This prints:
55
In the above program, the main()
function passes the address of x
to function print()
, which then uses the dereference operator (*)
to get the value at that address for printing. Note that we can pass either a pointer (assuming we have one), or use the address-of operator (&)
to get a pointer to the address of a normal variable.
Pass by address does not make a copy of the argument
Much like pass by reference
, when we pass an argument by address, no copy of the argument is made. Instead, we’re passing a copy of the argument’s address to the function (which is fast).
Here’s an example showing a std::string
passed by value, by reference, and by address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> #include <string> void printByValue(std::string str) // The function parameter is a copy of str { std::cout << str << '\n'; // print the value via the copy } void printByReference(std::string& ref) // The function parameter is a reference that binds to str { std::cout << ref << '\n'; // print the value via the reference } void printByAddress(std::string* ptr) // The function parameter is a pointer that holds the address of str { std::cout << *ptr << '\n'; // print the value via the dereferenced pointer } int main() { std::string str{ "Hello, world!" }; printByValue(str); // pass str by value, makes a copy of str printByReference(str); // pass str by reference, does not make a copy of str printByAddress(&str); // pass str by address, does not make a copy of str return 0; } |
Because copying a std::string
is expensive (it’s a compound type, not a fundamental type), we want to pass it by reference or by address to avoid making a copy when the function is called.
Pass by address allows the function to modify the argument’s value
Also much like pass by reference
, when we pass an argument by address, the function can modify the argument via the pointer parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> void changeValue(int* ptr) { *ptr = 6; // change the value to 6 } int main() { int x{ 5 }; std::cout << "x = " << x << '\n'; changeValue(&x); // we're passing the address of x to the function std::cout << "x = " << x << '\n'; return 0; } |
This prints:
5 6
As you can see, the argument is modified and this modification persists even after changeValue()
has finished running.
Pass by address allows passing optional or null parameters
HERE HERE
Let’s say you’re writing a function that has a parameter that may or may not exist (CLARIFY)
// show example with just value or reference
// show why pass by address is better
Here’s a more fully fleshed out (but less practical) example using std::string
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <string> void printName(std::string* ptr) // The function parameter is a pointer { if (ptr) // if ptr is not a null pointer std::cout << "Your name is " << *ptr << '\n'; else std::cout << "Your name is not known\n"; } int main() { std::string alex{ "Alex" }; printName(&alex); printName(nullptr); return 0; } |
The above program prints:
Your name is Alex Your name is unknown
Null checking
Now consider this fairly innocent looking program:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void print(int* ptr) { std::cout << *ptr; } int main() { int x{ 5 }; print(&x); print(nullptr); return 0; } |
When this program is run, it will print the value 5
and then most likely crash.
In the second call to print()
, we’re passing a nullptr
value, which makes parameter ptr
a null pointer. When this null pointer is dereferenced in the body of print()
, undefined behavior results.
When passing a parameter by address, care should be taken to ensure the pointer is not a null pointer before you dereference the value.
One way to do that is to use a conditional statement:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> void print(int* ptr) { if (!ptr) // if ptr is a null pointer, early return back to the caller return; std::cout << *ptr; } int main() { int x{ 5 }; print(&x); print(nullptr); return 0; } |
If a null pointer should never be passed to the function, an assert
(which we covered in lesson %Failed lesson reference, id 5845%) may be a better choice (as it explicitly documents that a null pointer is an invalid value):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> #include <cassert> void print(const int* ptr) // now a pointer to a const int { assert(ptr); // fail the program in debug mode if a null pointer is passed if (!ptr) // if ptr is a null pointer, early return back to the caller return; std::cout << *ptr; } int main() { int x{ 5 }; print(&x); print(nullptr); return 0; } |
Rule
All function parameters passed by pointer should be checked for null values (via a conditional statement or assert) before dereferencing them.
Passing by pointer to const
In the case of the print()
function from the prior example, since the function does not modify the value being pointed to, we really should make the function parameter a pointer to const
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> void print(const int* ptr) // now a pointer to a const int { if (!ptr) // if ptr is a null pointer, early return back to the caller return; std::cout << *ptr; } int main() { int x{ 5 }; print(&x); print(nullptr); return 0; } |
This allows us to tell at a glance that print()
won’t modify the argument passed in, and will ensure that we don’t do so by accident.
Prefer pass by reference
Note that function print
in the example above doesn’t handle null values very well -- it effectively just aborts the function. Given this, why allow a user to pass in a null value at all? Unless a function needs to handle both normal and null values, pass by reference
is generally preferable to pass by address
because references don’t allow null values.
Best practice
Prefer pass by reference
to pass by address
unless your function needs to handle null values or the ability to repoint at another address.
Pass by address helps makes modifiable parameters explicit
In lesson The C++ Tutorial (Test Site) we talked about how using references for in-out parameters can lead to problems recognizing that a function call may modify a value passed in:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> void doSomething(int mode) // this function does not modify the value of mode { // imagine code that does something useful with mode here } void doSomethingElse(int& mode) // this function modifies the value of mode { // imagine code that does something useful with mode here mode = 2; } int main() { int mode { 1 }; doSomething(mode); // perfectly safe doSomethingElse(mode); // deceptively evil // Programmer still expects mode to be 1 // But doSomethingElse changed it to 2! if (mode == 1) std::cout << "No threat detected.\n"; else std::cout << "Launching nuclear missiles...\n"; return 0; } |
Because of this, some companies (such as Google) have guidelines that state in-out parameters should be passed by address rather than by reference. Let’s update the program above to use pass by address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> #include <cassert> void doSomething(int mode) // this function does not modify the value of mode { // imagine code that does something useful with mode here } void doSomethingElse(int *mode) // now pass by address instead of by reference { // imagine code that does something useful with mode here mode = 2; } int main() { int mode { 1 }; doSomething(mode); // perfectly safe doSomethingElse(&mode); // now it is clear that this function might modify argument 'mode' if (mode == 1) std::cout << "No threat detected.\n"; else std::cout << "Launching nuclear missiles...\n"; return 0; } |
Passing by address requires the caller to pass in a normal variable argument using the address-of operator (&), which helps identify that the function could modify the argument just by looking at function call.
This has a few downsides: first, the function now has to deal with null pointers, which means we’ve traded an unlikely problem (the function modifying a value without the programmer realizing it) for possible program crashes or asserts. Second, it doesn’t work for pointers, which don’t need the explicit ampersand to be passed by address, and still allow modification of the value being pointed to.
Given this, our recommendation is to hold the course on preferring pass by reference for in-out parameters.
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 (which is fine, because copying an address is fast). As shown in the initial example, we can dereference that address to have the function change the value of the argument.
However, what if we instead change the address held by the parameter (e.g. repoint the parameter at something else)? The following program explores this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> void nullify(int* ptr2) { ptr2 = nullptr; // Make the function parameter a null pointer } int main() { int x{ 5 }; int* ptr{ &x }; std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n"); nullify(ptr); std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n"); return 0; } |
This program prints:
ptr is non-null ptr is non-null
As you can see, changing the address held by the pointer parameter had no impact on the address held by the argument (ptr
still points at x
).
If you think about it, this makes sense. When function nullify()
is called, ptr2
is instantiated as its own pointer object, and then initialized with the address passed in (in this case, the address held by ptr
, which is the address of x
). When the function changes what ptr2
points at, this only affects the ptr2
object, not the argument.
So what if we want to allow a function to change what a pointer argument points to?
Pass by pointer… by reference?
Yup, it’s a thing. Just like we can pass a normal variable by reference, we can also pass pointers by reference. Here’s the same program as above now with ptr2 changed to be a reference to an address:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> void nullify(int*& ptr2) // ptr2 is now a reference to a pointer { ptr2 = nullptr; // Make the function parameter a null pointer } int main() { int x{ 5 }; int* ptr{ &x }; std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n"); nullify(ptr); std::cout << "ptr is " << (ptr ? "non-null\n" : "null\n"); return 0; } |
This program prints:
ptr is non-null ptr is null
When an argument is passed as a reference to a pointer, any changes made to the function parameter (which is a reference) are reflected through to the argument. This allows functions to “repoint” an argument. We can see in the program above that the nullify
function is able to point ptr2
to null, which because ptr2
is a reference to ptr
, changes what ptr
points to.
As an aside...
Because references to pointers are fairly uncommon, it can be easy to mix up the syntax for a pointer to reference (is it “int*&” or “int&*”?). The good news is that if you do it backwards, the compiler will error that you can’t have a pointer to a reference (because pointers must hold the address of an object, and references aren’t objects). Then you can switch it around.
![]() |
![]() |
![]() |
Leave a Reply