Occasionally we get posts asking the differences between references
and pointers, and in the context of function calls, when to pass
parameters by reference, pointer, or value.
Difference between references and pointers
A pointer variable is a variable which holds the memory address of
some other variable. That "target" variable may be named or unnamed.
For example:
1 2 3
|
int i;
int* pInt = &i; // pInt "points to" i
int* pInt2 = new int; // pInt2 "points to" an unnamed int.
|
A reference variable is a variable which "refers to" another
named or unnamed variable. For example:
1 2 3 4 5 6 7 8
|
void foo( const std::string& str ) {}
std::string s1;
std::string& s1ref = s1; // s1ref "refers" to s1
// Here, we construct an unnamed, temporary string object to call foo.
// foo's "str" parameter now "refers to" this unnamed object.
foo( std::string( "Hello World" ) );
|
There are three critical attributes of pointers that differentiate them
from references.
1. You use pointer syntax to access the values "pointed to" by the pointer.
2. You can redirect the pointer to point it to a different "target" variable.
3. You can make a pointer point to nothing (ie, NULL pointer).
Examples:
1 2 3 4 5 6 7
|
int i, j;
int* pInt = &i; // pInt "points to" i
*pInt = 42; // This assigns the variable pointed to by pInt to 42
// So in other words, since pInt points to i, i now has
// the value 42.
pInt = &j; // This makes pInt now point to j instead of i.
pInt = NULL; // This makes pInt point to nothing.
|
Notice how use of an asterisk prior to the pointer variable accesses
the value being pointed to. This is called the
dereference
operator, a somewhat unfortunate name given that the language also
supports references and the dereference operator has nothing to do
with references.
Now to references. References have a couple of key characteristics
that differentiate them from pointers:
1. References must be initialized at the point of instantiation.
2. References must ALWAYS "refer to" a named or unnamed variable
(that is, you cannot have a reference variable that refers to
nothing, the equivalent of a NULL pointer).
3. Once a reference is set to refer to a particular variable, you
cannot, during its lifetime, "rebind" it to refer to a different
variable.
4. You use normal "value" syntax to access the value being referred to.
Lets look at some examples:
1 2 3 4 5
|
int i = 20, j = 10;
int& iref = i; // Instantiate iref and make it refer to i
iref = 42; // Changes the value of i to 42
iref = j; // Changes the value of i to 10 (the value of j)
iref = NULL; // Changes the value of i to 0.
|
So it seems like references are more limiting than pointers in that
two of the three characteristics of pointers are not available with
references. But in fact, these limitations tend to make programming
easier.
First of all, when writing generic template code, you can't easily write
a single template function that operates on values, references, and
pointers, because to access the value "pointed to" by a pointer requires
an asterisk whereas accessing a normal value does not require an asterisk.
Accessing the value of a reference works the same way as accessing a
normal value -- no asterisk needed. So writing templates that can handle
values and references is easy. Here's a real-world example:
1 2 3 4 5 6 7
|
template< typename T >
void my_swap( T& t1, T& t2 )
{
T tmp( t1 );
t1 = t2;
t2 = tmp;
}
|
The above function works great in these cases:
1 2 3 4 5
|
int i = 42, j = 10;
int& iref = i, jref = j;
my_swap( i, j ); // Sets i = 10 and j = 42
my_swap( iref, jref ); // Swaps i and j right back
|
But, if you are expecting with the following code that i will be set
to 10 and j to 42, then this doesn't do what you want:
1 2 3 4
|
int i = 42, j = 10;
int* pi = &i, *pj = &j;
my_swap( pi, pj ); // sets pi = &j and pj = &i
|
Why? Because you need to dereference the pointers to get to the
values being pointed to, and the template function above does not
have a single asterisk in it.
If you wanted this to work correctly, you'd have to write:
1 2 3 4 5 6 7
|
template< typename T >
void my_ptr_swap( T* t1, T* t2 ) // There are other ways to declare this
{
T tmp( *t1 );
*t1 = *t2;
*t2 = tmp;
}
|
And now in the above example, you'd use
my_ptr_swap( pi, pj );
to swap the values pointed to by pi and pj. Personally, I think this
solution stinks for three reasons. First, I have to remember two function
names instead of one: my_ptr_swap and my_swap. Second, my_ptr_swap is
harder to understand than my_swap because although they have the same number
of lines of code and effectively do the same thing, there are extra
deferences involved. (And I almost implemented the function wrong when I
wrote it). Thirdly, NULL pointers! What happens if one or both of the
pointers you pass to my_ptr_swap are NULL? Nothing good. In reality, if
I wanted to make my_ptr_swap robust, to avoid the crash, I'd have to write:
1 2 3 4 5 6 7 8 9 10
|
template< typename T >
void my_ptr_swap( T* t1, T* t2 ) // There are other ways to declare this
{
if( t1 != NULL && t2 != NULL )
{
T tmp( *t1 );
*t1 = *t2;
*t2 = tmp;
}
}
|
But this isn't exactly a great solution either, I suppose, because now
the caller of my_ptr_swap cannot be 100% sure the function did anything
unless they duplicate the if() check:
1 2 3 4
|
if( pi != NULL && pj != NULL )
my_ptr_swap( pi, pj );
else
std::cout << "Uh oh, my_ptr_swap won't do anything!" << std::endl;
|
But duplicating the check makes the check inside of my_ptr_swap kinda
pointless. But on the other hand, a function should always validate its
arguments. A conundrum. Perhaps a return value is in order:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
template< typename T >
bool my_ptr_swap( T* t1, T* t2 ) // There are other ways to declare this
{
if( t1 != NULL && t2 != NULL )
{
T tmp( *t1 );
*t1 = *t2;
*t2 = tmp;
return true;
}
return false; // false means function didn't swap anything
}
|
And now I can write:
1 2
|
if( my_ptr_swap( pi, pj ) == false )
std::cout << "Uh oh, my_ptr_swap won't do anything!" << std::endl;
|
Which is definitely better. But this simply introduces an extra error
leg in your program that you need to think about and deal with.
What
if my_ptr_swap fails? What should I do? In most trivial applications,
handling errors, if even done at all, are easy. But in larger applications
where you need to perform a sequence of 5 steps, each of which may fail
means you have to think about all of the following error legs:
1. What if operation #1 fails?
2. What if operation #2 fails? Do I roll back operation #1? What if
rolling back operation #1 fails?
3. What if operation #3 fails? Do I roll back #1 and #2? What if
rolling back operation #2 succeeds but #1 fails? What if rolling
back operation #2 fails?
4. What if operation #4 fails? .... etc ...
5. What if operation #5 fails? .... etc ...
There are a mind-boggling number of failure cases to think about
and test. Because it quickly gets complicated, most programmers
handle only one failure; double faults are often not handled very
gracefully (the program aborts in some way).
It seems to me that since error handling can easily dominate the design
effort and implementation effort, programmers should strive NOT to
artificially introduce error legs where they can easily be avoided.
One of the most prevalent error cases to deal with is that of a NULL
pointer. Enter references.
(to be continued)