Monday, January 17, 2011

Passing arguments by address

There is one more way to pass variables to functions, and that is by address. Passing an argument by address involves passing the address of the argument variable rather than the argument variable itself. Because the argument is an address, the function parameter must be a pointer. The function can then dereference the pointer to access or change the value being pointed to.
Here is an example of a function that takes a parameter passed by address:
01void foo(int *pValue)
02{
03    *pValue = 6;
04}
05 
06int main()
07{
08    int nValue = 5;
09 
10    cout << "nValue = " << nValue << endl;
11    foo(&nValue);
12    cout << "nValue = " << nValue << endl;
13    return 0;
14}
The above snippet prints:
nValue = 5
nValue = 6
As you can see, the function foo() changed the value of nValue through the pointer parameter pValue.
Pass by address is typically used with dynamically allocated variables and arrays. For example, the following function will print all the values in an array:
1void PrintArray(int *pnArray, int nLength)
2{
3    for (int iii=0; iii < nLength; iii++)
4        cout << pnArray[iii] << endl;
5}
Here is an example program that calls this function:
1int main()
2{
3    int anArray[6] = { 6, 5, 4, 3, 2, 1 };
4    PrintArray(anArray, 6);
5}
This program prints the following:
6
5
4
3
2
1
Note that the length of the array must be passed in as a parameter, because arrays don’t keep track of how long they are. Otherwise the PrintArray() function would not know how many elements to print.
It is always a good idea to ensure parameters passed by address are not null pointers before dereferencing them. Dereferencing a null pointer will typically cause the program to crash. Here is our PrintArray() function with a null pointer check:
1void PrintArray(int *pnArray, int nLength)
2{
3    // if user passed in a null pointer for pnArray, bail out early!
4    if (!pnArray)
5        return;
6 
7    for (int iii=0; iii < nLength; iii++)
8        cout << pnArray[iii] << endl;
9}
Advantages of passing by address:
  • It allows us to have the function change the value of the argument, which is sometimes useful
  • Because a copy of the argument is not made, it is fast, even when used with large structs or classes.
  • We can return multiple values from a function.
Disadvantages of passing by address:
  • Because literals and expressions do not have addresses, pointer arguments must be normal variables.
  • All values must be checked to see whether they are null. Trying to dereference a null value will result in a crash. It is easy to forget to do this.
  • Because dereferencing a pointer is slower than accessing a value directly, accessing arguments passed by address is slower than accessing arguments passed by value.
As you can see, pass by address and pass by reference have almost identical advantages and disadvantages. Because pass by reference is generally safer than pass by address, pass by reference should be preferred in most cases.
Passing by reference, address, and value is actually not so different
Now that you understand the basic differences between passing by reference, address, and value, let’s complicate things by simplifying them. :)
In the lesson on passing arguments by reference, we briefly mentioned that references are typically implemented by the compiler as pointers. Because of this, the only real difference between pointers and references is that references have a cleaner but more restrictive syntax. This makes references easier and safer to use, but also less flexible. This also means that pass by reference and pass by address are essentially identical in terms of efficiency.
Here’s the one that may surprise you. When you pass an address to a function, that address is actually passed by value! Because the address is passed by value, if you change the value of that address within the function, you are actually changing a temporary copy. Consequently, the original pointer address will not be changed!
Here’s a sample program that illustrates this.
01#include <iostream>
02 
03int nFive = 5;
04int nSix = 6;
05 
06// Function prototype so we can define
07// SetToSix below main()
08void SetToSix(int *pTempPtr);
09 
10int main()
11{
12    using namespace std;
13 
14    // First we set pPtr to the address of nFive
15    // Which means *pPtr = 5
16    int *pPtr = &nFive;
17 
18    // This will print 5
19    cout << *pPtr;
20 
21    // Now we call SetToSix (see function below)
22    // pTempPtr receives a copy of the address of pPtr
23    SetToSix(pPtr);
24 
25    // pPtr is still set to the address of nFive!
26    // This will print 5
27    cout << *pPtr;
28 
29    return 0;
30}
31 
32// pTempPtr copies the value of pPtr!
33void SetToSix(int *pTempPtr)
34{
35    using namespace std;
36 
37    // This only changes pTempPtr, not pPtr!
38    pTempPtr = &nSix;
39 
40    // This will print 6
41    cout << *pTempPtr;
42}
Because pTempPtr receives a copy of the address of pPtr, even though we change pTempPtr, this does not change the value that pPtr points to. Consequently, this program prints
565
Even though the address itself is passed by value, you can still dereference that address to permanently change the value at that address! This is what differentiates pass by address (and reference) from pass by value.
The next logical question is, “What if we want to be able to change the address of an argument from within the function?”. Turns out, this is surprisingly easy. You just use pass the pointer itself by reference (effectively passing the address by reference). You already learned that values passed by reference reflect any changes made in the function back to the original arguments. So in this case, we’re telling the compiler that any changes made to the address of pTempPtr should be reflected back to pPtr! The syntax for doing a reference to a pointer is a little strange (and easy to get backwards): int *&pPtr. However, if you do get it backwards, the compiler will give you an error.
The following program illustrates using a reference to a pointer.
01// pTempPtr is now a reference to a pointer to pPtr!
02// This means if we change pTempPtr, we change pPtr!
03void SetToSix(int *&pTempPtr)
04{
05    using namespace std;
06 
07    pTempPtr = &nSix;
08 
09    // This will print 6
10    cout << *pTempPtr;
11}
Note that you’ll also have to update the function prototype above main to account for the new prototype of SetToSix():
1// Function prototype so we can define
2// SetToSix below main()
3void SetToSix(int *&pTempPtr);
When we run the program again with this version of the function, we get:
566
Which shows that calling SetToSix() did indeed change the address of pPtr!
So strangely enough, the conclusion here is that references are pointers, and pointer addresses are passed by value. The value of pass by address (and reference) comes solely from the fact that we can dereference addresses to change the original arguments, which we can not do with a normal value parameter.

No comments: