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 using call-by-address.

Address (or "Pointer") Variables

Up to now variables have held

Now we will look at a class of variables that hold addresses, and at some of their uses. 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: &

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 calledr 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 ris 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 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()? 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 as follows:

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.


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 call by address

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)++;  
  }

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;

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