CSCI 240 Lecture Notes - Part 9


We have seen that when passing simple variables to a function, the function cannot change those variables.

We have also seen that when passing arrays to a function, the function can change the values in the array - in part because the thing passed is the address of the array.

It turns out that in C++ a function can change the values of simple variables passed to it.  We have not told you the whole truth - yet.  The whole truth is that in C++ there are two slightly different ways to arrange this. 

When you (the programmer) design a new function, you are in control of when and how you will do this and when you won't.  When you use an existing function, someone else has already decided. So you must understand both ways to properly supply the arguments if you want to use other people's functions..

Why would you want to have a function change the value of an argument passed to it?  Suppose the purpose of the function was to calculate two or more answers (results).  With the pass-by-value mechanism we have used up to now, you can't change even one simple variable, and you can return at most one value.  What if you want two or three answers?

As a simple example, suppose you want to write a function that calculates both the quotient and the remainder of an integer division.  We would like to be able to call it something like this:

divide(divisor, dividend, quotient, remainder);

and have the answers stored into quotient and remainder by the function.

We can do that.  One way is by using call-by-reference, the other is call-by-address

We will cover both.  Call-by-reference is new in C++ and is a bit easier.  Call-by-address is used in C and is also supported in C++.  You need to know both (sorry!) because both are used in C++ programming.  For example, although we didn't make a point of it, some of the Allegro functions use call-by-address, so to fully understand how to use them, you need to understand call-by-address.

Call-by-Reference

To pass an argument to a function by reference so that the function can change it, you must

Here is an example:

Caller:

// prototype
void fn(int&);
// variable declaration
int num;
// function call
fn(num);

Function:

void fn(int &i)
  {
  // actually stores 10 into num back in the caller
  i = 10;
  }

Notice that if you look at the function call itself, you can't tell if it is pass-by-value or pass-by-reference.  Only by looking at the function prototype or header can you tell that the argument is passed by reference so it can be altered.  Likewise, if you look in the function body you can't tell.  The only way to tell is to look at the function prototype or header.  That's what the C++ compiler looks at.  When it sees that & it will (quietly) arrange to pass the address of the variable to the function, so the function can access the original.  So pass-by-reference is really passing an address, but it's done automatically when you use the &.

Here is the divide() function and calling code mentioned above:

Caller:

// prototype
void divide(int, int, int&, int&);
// declarations
int num1 = 20,
    num2 = 6,
    quo,
    rem;
....
// function call
divide(num2, num1, quo, rem);
// now quo has a value of 3 and rem has a value of 2

Function:

void divide(int dvs, int dvd, int& q, int& r)
  {
  q = dvd / dvs;
  r = dvd % dvs;
  }

Notice that we passed the divisor and dividend by value since we just wanted to use their values and not alter them.  We passed the quotient and remainder by reference since the purpose of the function is to alter both of these values.  Notice, too, that quotient and remainder were not initialized by the caller since their values are never actually used in the function, they are just assigned values.  But the divisor and dividend were initialized since their values were used to compute the answers.

More examples of functions that take reference arguments are provided later.

Reference Variables

We will not have much use for this topic in this course, but you should be aware of it: it is possible to declare a special variable type that can hold a reference to another simple variable:

int a = 4;
int& b = a;
b = 5;

What does this mean?

Line 1: declare a simple int variable a, and give it a value of 4.
Line 2: declare a reference variable, b,  and make it "refer to" an int variable, a.  The int& denotes a new data type, one that can hold a reference to an int.  In a sense, b is a alias for a.
Line 4: when we assign 5 to b, we really are assigning 5 to what b refers to, which is aa now has the value 5.

We can make reference variables of any simple data type.  You may revisit this topic in future courses.

But first, let's approach pass-by-address functions by starting with...

Address (or "Pointer") Variables

Up to now variables have held
  • values of some quantity, (represented as integers, characters, strings, etc.) usually some real-world thing, or
  • a reference to a regular variable (see section above) or,
  • as we saw earlier, for unsubscripted array names, the address in memory of the beginning of the array.

Now we will look at a fourth class of variables that hold addresses, and at some of their uses.

Whereas reference variables hid some details from us, with address (sometimes called pointer) variables we are in charge of (almost) all of the details, so we need to examine them in more detail.

For every simple data type (int, char, etc.), you can create a variable that holds the address of another variable of that data type.

int i;	// can hold an integer value
 
int *p;	// can hold the address of an int variable
 

Note this syntax carefully. The int * denotes a new data type, one that can hold the address of an int.

We read this as "p is a pointer to an int" or "p holds the address of an int"

Suppose that i is stored at address 200, and i's value is 4.

Suppose further that p (somehow) already holds the address of i. Then p's value is 200.

We say that "p points to i".

How can we get the address of a simple variable (so that we could store it into a pointer variable)? There is a special C++ operator that will get it: &

But it is not the same as the & used in reference variable declarations or call-by-reference function calls.  This is confusing.  You just have to keep the two sets of rules separate.  It also has nothing to do with the && of compound conditions.

Suppose we have the following declarations:

int i;	 // can hold an  int
int *p;	 // can hold the address of an  int variable
i = 4;	 // store the value 4 in variable  i
p = &i;	 // find and store the address of  i into p.

In this last line, we have put the address of a simple variable into a pointer variable.  Now, "p points to i" or "p contains the address of i"

What good does this do?  The most important uses will come later, but for right now, we can use this together with one more new idea to create a new way to use the value in a simple variable.

We know we can access the value in i  by using i itself; for example:

cout << i;

Now we can use the the pointer variable to get to i's value (assuming as above that p points to i). This will prove very useful soon.

But first we have to know how we can access the value stored in i by using the pointer p? We dereference the pointer.

The dereference operator  is the *. Write the * before the pointer and you have an expression that refers to the "value pointed to" by the pointer.
So given the declarations and assignments above, we can:

cout << i;  // 4
cout << *p; // 4
 

In the first, we print the value stored in variable i

In the second, we print the value stored in the int variable pointed to by p (which is the value in i)

They are the same thing.

Notice another possible confusion:

In a declaration, you write:

int *p;
 

This declares p to be a variable that can hold the address of an integer variable.   Think of "int *" as the data type of p.

In contrast, in an executable statement you might write:

x = *p;

Here, *p refers to "the value in the variable whose address is stored in p" or more briefly, "the value pointed to by p"

So you see "*p" in two different contexts -

  1. in a declaration, where the * really should be considered part of the data type "int *" or "double *" or whatever
  2. in a dereference where the expression evaluates to the value stored at the place where the pointer variable points to

Once again, what good does all this do? If we can use i, why bother with this pointer dereferencing?

In the examples so far, they don't do any good; that is, they are only a more complicated way to do some things we already can do with simple variables.

However, there are several very important uses for this feature in C++. In particular, they make it possible to alter the values passed to a function in the call-by-address mechanism.


Passing Addresses to Functions as Arguments

Recall:
  • passing a simple data type to a function passes a copy. The original cannot be altered.
  • passing an array to a function passes the address of the array, and so values in the original array can be altered.
  • passing a reference gives us one way to alter the values of simple variables passed to a function.  References are disguised addresses, which are simpler to use than "bare" addresses, but the central concept here is:

Passing an argument by address allows alteration of the original data - because code in the function can "get to" the original value.

Let's redo divide() using these new address/pointer concepts:

Caller:

void divide(int, int, int *, int *);
int num1 = 20,
    num2 = 6,
    quo,
    rem;
....
divide(num2, num1, &quo, &rem);
// now quo has a value of 3 and rem has a value of 2

Function:

void divide(int dvs, int dvd, int *q, int *r)
  {
  *q = dvd / dvs;
  *r = dvd % dvs;
  }

Note the way the fucntion arguments are declared: "the argument called q is a pointer to an int; the argument called r is also a pointer to an int".

And because of the way we called the function in main(), the argument called q is a pointer to main's quo and the argument called r is a pointer to main's rem.  We are passing the addresses of quo and rem by using the &-notation.

Read the two lines in the function as: "the thing pointed to by q gets the result of dvd/dvs" and "the thing pointed to by r gets the result of dvd%dvs"

Since q is the address of quo (in main()) and r is the address of rem (in main()), quo and rem get the values. And nothing is "returned".


We often say that a function "passes back values" into variables passed by reference or address as opposed to a function "returning" a value via the return mechanism.

Note again that when you pass an array name, you are passing an address - that's the way C++ defines it. You don't need to (and should not) use the & when passing an array.

Note, too, that if you have an array, ar, then

ar is exactly the same thing as &ar[0].

ar - the unsubscripted name of an array is the address of the beginning of the array.   The notation &ar should never be used.

&ar[0] is the "address of the 0th element in the array" = = ar

Once again, however, if you want to pass a particular array element, you do need the & because only the unsubscripted name of an array is equivalent to its address.

For example, remember swap() as explained in lecture? It could be written as a function as follows:

void swap(int a[], int top, int small)
  {
  int temp;
  temp = a[top];
  a[top] = a[small];
  a[small] = temp;
  }
 

In this case we had to pass the array and two integers to serve as the subscripts of the elements we wanted to swap, because the only way to use a function to change a part of the array was to pass the whole thing.

We would call it as follows:

swap(ar, top, s);

Now, however, we could write it in either of these two ways:

By address:
void aSwap(int *i, int *j)
  {
  int temp;
  temp = *i;
  *i = *j;
  *j = temp;
  }

And it would be called by code like this:

aSwap(&ar[top], &ar[s]);

In fact, any two integers in memory could be swapped using this function now, not just integers in a particular array.

int first = 4;
int second = 6;
 
aSwap(&first, &second);

Now first has 6 and second has 4.

By reference:
void rSwap(int& i, int& j)
  {
  int temp;
  temp = i;
  i = j;
  j = temp;
  }

And it would be called by code like this:

rSwap(ar[top], ar[s]);

In fact, any two integers in memory could be swapped using this function now, not just integers in a particular array.

int first = 4;
int second = 6;
 
rSwap(first, second);

Now first has 6 and second has 4.


 Study these two carefully.  Further examples follow shortly.


Address/Pointer Summary

To declare a simple variable:

Format:
data-type data-name;

Example:
char ch = 'a'; //declared and given a value

To declare a pointer variable:

Format:
data-type-pointed-to *name-of-var;

Example:
char *cp; // cp can hold pointer to a char

To obtain the address of a variable:

Format:
&data_name

Example:
cp = &ch; // cp now holds the address of ch

To dereference a pointer variable (i.e. to get the value it points to)

Format:
*ptr_name

Example:
cout << *cp; // prints contents of ch: a

To design a function that takes an address/pointer argument to simple data types:

Example: a function to pass back the cube of its double argument via a pointer argument:

in calling function:

double num = 3.0;
 
cube(&num);
 

The cube function prototype:

void cube(double *);
 

The cube() function itself:

void cube(double *n)
  {
  *n = (*n)*(*n)*(*n);  // note use of parens
  }
 

Further Examples of calls by address and reference.

In each of the following examples, code for call-by-address and call-by-reference is shown.  As you study these, you might consider covering one version while you read the other.  Then, before you uncover the covered version, see if you can write it.  Alternate which version you cover as you go through them.

Example 1: suppose we have a program that prints (on paper) a multipage report. We want a function to print a title or header at the top of each new page, complete with page number.

We will declare a page number variable in main(), but would like the function to increment it automatically each time it is called.

void pgHeader(int *);
 
void main()
  {
  int pageNum = 1;
  
  while (…)
    {
     // more code ...
     if (time to print pg hdr)
        pgHeader(&pageNum); 
    }
  }
 
void pgHeader(int *pnum)
  {
  cout << "The Title " << *pnum;
  //increment pageNum in main() 
  //Note the () around *pnum
  (*pnum)++;  
  }
void pgHeader(int&);
int main()
  {
  int pageNum = 1;
  while (...)
    {
    // more code ...
    if (time to print a pg hdr)
      pgHeader(pageNum);
    }
  }
void pgHeader(int& pnum)
  {
  cout << "The Title " << pnum;
  //increment pageNum in main()
  pnum++;
  }
 

 Note: we could alternately have designed pgHeader() to return an incremented page number (the old pass-by-value way). For comparison, here it is:

int pgHeader(int);
 
void main()
  {
  int pageNum = 1;
  
  while (…)
    {
     // code to do something useful…
     if (it's time to print a page header)
        pageNum = pgHeader(pageNum);  
    }
  }
 
int pgHeader(int pnum)
  {
  cout << "The Title " <<  pnum;
  pnum++;  // increment copy
  return (pnum); // new value of page number
  }

Example 2: suppose we have a sum and a count, and want to write a function to calculate an average. However, sometimes the count will be zero, so we also want the function to tell us if it was able to successfully accomplish its task.

We could design the function to return a value indicating success or failure, and to pass back the calculated average (when count != 0).

We will return 0 to indicate failure and 1 to indicate success.

Let's write the function first:
int calcAvg(int sum, 
            int count, 
            double *avg)
  {
  if (count == 0)
    return (0);
  else
    {
    *avg = (double)sum / count;
    return (1);
    }
  }
 

Then in the calling function:

int total;
int c;
int rc;
double avg;
 
…
 
// get values into total and c
rc = calcAvg(total, c, &avg);
if (rc = = 0)
  cout << "no average calc'd";
else
  cout << "Average is " << avg;
The function first:
int calcAvg(int sum, 
            int count, 
            double& avg)
  {
  if (count == 0)
    return (0);
  else
    {
    avg = (double)sum / count;
    return (1);
    }
  }
 

And the calling function:

int total;
int c;
int rc;
double avg;
 
…
 
// get values into total and c
rc = calcAvg(total, c, avg);
if (rc = = 0)
  cout << "no average calc'd");
else
  cout << "Average is " << avg);

Here, in both cases, the function is returning one value (the return code) and passing back one value - the calculated average, when the calculation is performed, i.e. when count is not 0.  If the count is 0, the avg argument is not altered.


Study these examples until they all seem clear to you.  Notice the patterns that are common to each type of function call.  This may take some time.  You will probably find it helpful to come back to them several times.  Each time, you will be able to understand them more quickly.  After you have done this, try writing your own similar functions (both ways).

Here are a few sample exercises to try.

1. Write a function that takes an array of integers and passes back the highest and lowest value in the array.

2. Write a function that takes an array of doubles and passes back the sum and the average of the numbers in the array.

3. Write a function that takes an integer number and passes back the sum of the integers up to that number.  For example, passing in 5 would pass back 1 + 2 + 3 + 4 + 5 = 15