CS 102 / ECE 206 Lecture Notes for Chapter 8


1. Pointers

"Do not mistake the pointing finger for the moon."   -ZEN SAYING

Pointers are one of the most powerful features of C++, and the book has very nice text on them. The basic vibe is this: A variable such as an int consumes storage. You access that storage by using the variable name. A pointer is a variable that points to the storage of a variable. Sometimes we call this the "address" of a variable. To access the storage, you need to "dereference" the pointer. An example (in pex1.cpp):


  #include <iostream>
  using namespace std;

  int main()
  {
    int i;
    int *i_ptr;

    i = 5;         // Set the value of i
    i_ptr = &i;    // Set i_ptr to be the address of i  

    cout << "i      is: " << i << endl;
    cout << "&i     is: " << &i << endl;
    cout << "i_ptr  is: " << i_ptr << endl;
    cout << "*i_ptr is: " << *i_ptr << endl;
  
    *i_ptr = 25;

    cout << endl;
    cout << "i      is: " << i << endl;
    cout << "*i_ptr is: " << *i_ptr << endl;

    return 0;
  }

You'll note, you access the address of i with &i, and you dereference the pointer i_ptr with *i_ptr. Here is the output of this when run on my office desktop (different machines will print different pointer values):

  UNIX> g++ pex1.cpp
  UNIX> g++ -o pex1 pex1.cpp
  UNIX> pex1
  i      is: 5
  &i     is: 0xbf928b8c
  i_ptr  is: 0xbf928b8c
  *i_ptr is: 5

  i      is: 25
  *i_ptr is: 25
  UNIX> 
The value of i_ptr is not overly important, except that it is the same as the address of i. Since both i and *i_ptr access the same storage, changing one of them changes the other. This is reflected in the last four lines of the program -- as you can see, when we change *i_ptr, i is changed as well.

2. Pointers in Functions

When you pass a pointer as a parameter to a function, then when you dereference it in the function, you will modify whatever it points to. This causes a side effect. For example, in the code below (pex2.cpp), the procedure a() modifies i in main() because it has been passed a pointer to i's storage:


  #include <iostream>
  using namespace std;

  void a(int *i_ptr)
  {
    *i_ptr = 12;
  }

  int main()
  {
    int i;
    int *i_ptr;

    i = 5;         
    i_ptr = &i;   

    cout << "Before calling a(): i is: " << i << endl;  
    a(i_ptr);
    cout << "After  calling a(): i is: " << i << endl;  
    return 0;
  }

Here's it running: you'll see that indeed i is modified by the call to a():

  UNIX> g++ -o pex2 pex2.cpp
  UNIX> pex2
  Before calling a(): i is: 5
  After  calling a(): i is: 12
  UNIX> 

One of the biggest uses of pointers is to in effect return two things from a function. For example, the function below determines both the square and the cube of a number: (In pex3.cpp),


  #include <iostream>
  using namespace std;

  void square_and_cube(int n, int *n_squared, int *n_cubed)  
  {
    *n_squared = n * n;
    *n_cubed = n * n * n;
  }

  int main()
  {
    int n, sq, cube;

    cout << "Enter n: ";
    cin >> n;

    square_and_cube(n, &sq, &cube);

    cout << n << " squared is " << sq << endl;
    cout << n << " cubed   is " << cube << endl;
  
    return 0;
  }

Here's it running: you'll see that indeed i is modified by the call to a( ):

  UNIX> g++ -o pex3 pex3.cpp
  UNIX> pex3
  Enter n: 5
  5 squared is 25
  5 cubed   is 125
  UNIX> 


3. Pass By Value -- Always!! (with one small exception)

I can't stress this enough -- regardless of what the book says, C++ and C usually pass arguments by value. That means that they copy their arguments when they pass them to procedures. Which means that when the procedure changes the arguments' values, it does not affect the original copies. Note: reference variables are the exception.

For example, in the program below, even though the procedure a( ) changes i, that does not modify the variable i in main(). This is because i has been "passed by value." (In pex4.cpp).


  #include <iostream>
  using namespace std;

  void a(int i)
  {
    i = 7;
    cout << "Inside a().          i = " << i << " and &i = " << &i << endl;  
  }

  int main()
  {
    int i;

    i = 5;

    cout << "Before calling a().  i = " << i << " and &i = " << &i << endl;  
    a(i);
    cout << "After  calling a().  i = " << i << " and &i = " << &i << endl;  
  
    return 0;
  }

Here's it running. I've printed out the addresses of i in both a() and main() -- you can see that they are different. That is because they point to different storage -- i in a() is different from i in main(). That is why changing i in a() does not modify i in main():

  UNIX> g++ -o pex4 pex4.cpp
  UNIX> pex4
  Before calling a().  i = 5 and &i = 0xbffff74c
  Inside a().          i = 7 and &i = 0xbffff730
  After  calling a().  i = 5 and &i = 0xbffff74c
  UNIX> 

Now, the same thing is true when you pass a pointer as an argument to a procedure -- a copy is made of the pointer. However, when you deference the copy, that will change whatever the pointer points to. For example, look at the following program: (In pex5.cpp).


  #include <iostream>
  using namespace std;

  void a(int *p1, int *p2)
  {
  
    cout << endl << "Inside a()" << endl << endl ;

    cout << "p1 = " << p1 << " and &p1 = " << &p1 << " and *p1 = " << *p1 << endl;
    cout << "p2 = " << p2 << " and &p2 = " << &p2 << " and *p2 = " << *p2 << endl;

    p1 = p2;

    *p1 = 3;
    *p2 = 4;

    cout << endl << "Inside a() - after setting p1 to p2" << endl << endl ;

    cout << "p1 = " << p1 << " and &p1 = " << &p1 << " and *p1 = " << *p1 << endl;
    cout << "p2 = " << p2 << " and &p2 = " << &p2 << " and *p2 = " << *p2 << endl;
  }

  int main()
  {
    int i, j, *ptr1, *ptr2;

    ptr1 = &i;
    ptr2 = &j;
    i = 5;
    j = 6;

    cout << "Before calling a()" << endl << endl ;
  
    cout << "i = " << i << " and &i = " << &i << endl;
    cout << "j = " << j << " and &j = " << &j << endl;
    cout << endl;
    cout << "ptr1 = " << ptr1 << " and &ptr1 = " << &ptr1 << " and *ptr1 = " << *ptr1 << endl;  
    cout << "ptr2 = " << ptr2 << " and &ptr2 = " << &ptr2 << " and *ptr2 = " << *ptr2 << endl;  
  
    a(ptr1, ptr2);

    cout << endl << "After calling a()" << endl << endl ;  
  
    cout << "i = " << i << " and &i = " << &i << endl;  
    cout << "j = " << j << " and &j = " << &j << endl;  
    cout << endl;
    cout << "ptr1 = " << ptr1 << " and &ptr1 = " << &ptr1 << " and *ptr1 = " << *ptr1 << endl;  
    cout << "ptr2 = " << ptr2 << " and &ptr2 = " << &ptr2 << " and *ptr2 = " << *ptr2 << endl;  
  
    return 0;
  }

Here's it running. Study this carefully:

  UNIX> g++ -o pex5 pex5.cpp    
  UNIX> pex5
  Before calling a()

  i = 5 and &i = 0xbffff73c
  j = 6 and &j = 0xbffff738

  ptr1 = 0xbffff73c and &ptr1 = 0xbffff734 and *ptr1 = 5
  ptr2 = 0xbffff738 and &ptr2 = 0xbffff730 and *ptr2 = 6

  Inside a()

  p1 = 0xbffff73c and &p1 = 0xbffff720 and *p1 = 5
  p2 = 0xbffff738 and &p2 = 0xbffff724 and *p2 = 6

  Inside a() - after setting p1 to p2

  p1 = 0xbffff738 and &p1 = 0xbffff720 and *p1 = 4
  p2 = 0xbffff738 and &p2 = 0xbffff724 and *p2 = 4

  After calling a()

  i = 5 and &i = 0xbffff73c
  j = 4 and &j = 0xbffff738

  ptr1 = 0xbffff73c and &ptr1 = 0xbffff734 and *ptr1 = 5
  ptr2 = 0xbffff738 and &ptr2 = 0xbffff730 and *ptr2 = 4
  UNIX> 

Ok -- ptr1 equals the address of i and ptr2 equals the address of j. When we pass them to a(), copies are made -- p1 is set to ptr1 and p2 is set to ptr2. This is reflected in the output in the beginning of a(), where both ptr1 and p1 equal 0xbffff73c -- the address of i. You'll note, though, that ptr1 and p1 have different addresses (0xbffff734 vs. 0xbffff720). That is because p1 is a copy of ptr1

When we modify p1 by setting it equal to p2, you'll see that both of them have values of 0xbffff738 -- the address of j. So they both modify j by setting it to three, then 4. When a() finishes, i is unchanged, but j is 4. Moreover, you'll notice that ptr1 had not been modified by the call to a(). That is because arguments are passed by value -- even pointers!


4. Const

The const qualifier tells the compiler that the value of a variable should not be modified. Use constants to protect your values whenever possible.


      Example:  const int COUNT = 101;                                                      // COUNT cannot be modified
                        const char name[COUNT] = "J. Wallace Mayo";         // elements of name cannot be modified


5. The array/pointer duality

An array variable is a pointer to the first element of an array. For that reason, you may set a pointer equal to an array, and that has the pointer point to the first element of the array. Here's pex6.cpp:


  #include <iostream>
  using namespace std;

  int main()
  {
    int array[10];
    int *a_ptr;

    a_ptr = array;

    array[0] = 15;
    cout << "array[0]: " << array[0] << endl;  
    cout << "*a_ptr: " << *a_ptr << endl;  
    return 0;
  }

When it runs, you can see that dereferencing a_ptr is the same as accessing a[0].

  UNIX> g++ -o pex6 pex6.cpp
  UNIX> pex6
  array[0]: 15
  *a_ptr: 15
  UNIX> 

In fact, you can use standard array accessing syntax (square brackets) on the pointer, just like you do with an array. You can see this in pex7.cpp:


  #include <iostream>
  using namespace std;

  int main()
  {
    int array[10];
    int *a_ptr;
    int i;

    a_ptr = array;

    for (i = 0; i < 10; i++) a_ptr[i] = 30 + i;

    for (i = 0; i < 10; i++) {
      cout << "i: " << i << " - array[" << i << "]: " << array[i]
                    << " - a_ptr[" << i << "]: " << a_ptr[i] << endl;  
    }
    return 0;
  }

As you can see, you can treat a_ptr just like array.

  UNIX> g++ -o pex7 pex7.cpp
  UNIX> pex7
  i: 0 - array[0]: 30 - a_ptr[0]: 30
  i: 1 - array[1]: 31 - a_ptr[1]: 31
  i: 2 - array[2]: 32 - a_ptr[2]: 32
  i: 3 - array[3]: 33 - a_ptr[3]: 33
  i: 4 - array[4]: 34 - a_ptr[4]: 34
  i: 5 - array[5]: 35 - a_ptr[5]: 35
  i: 6 - array[6]: 36 - a_ptr[6]: 36
  i: 7 - array[7]: 37 - a_ptr[7]: 37
  i: 8 - array[8]: 38 - a_ptr[8]: 38
  i: 9 - array[9]: 39 - a_ptr[9]: 39
  UNIX> 

The reason why this is important is that when you pass an array to a function as an argument, you are passing a pointer to its first element. This means that if you change the contents of the array inside the function, that will change the contents of the original array. This is because only the pointer to the contents is copied in the function call. It is important to understand this.

So look at pex8.cpp:


  #include <iostream>
  using namespace std;

  void initialize_array(int *a)
  {
    int i;

    for (i = 0; i < 10; i++) a[i] = 40+i;
  }

  void display_array(int *a)
  {
    int *p;   //traveling pointer 

    cout << "Inside display_array:  ";
    for (p = a; p < a + 10; p++)  
      cout << *p << " ";
    cout << endl << endl;
  }
  
  int main()
  {
    int array[10];
    int i;
  
    initialize_array(array);

    display_array(array);
  
    for (i = 0; i < 10; i++) {
      cout << "i: " << i << " - array[" << i << "]: "   
           << array[i] << endl;  
    }
    return 0;
  }

When you run it, you'll see that initialize_array() changes the contents of array. That is because a is a pointer to the first element of array. Also, study the pointer arithmetic in display_array().

  UNIX> g++ -o pex8 pex8.cpp
  UNIX> pex8
  Inside display_array:  40 41 42 43 44 45 46 47 48 49

  i: 0 - array[0]: 40
  i: 1 - array[1]: 41
  i: 2 - array[2]: 42
  i: 3 - array[3]: 43
  i: 4 - array[4]: 44
  i: 5 - array[5]: 45
  i: 6 - array[6]: 46
  i: 7 - array[7]: 47
  i: 8 - array[8]: 48
  i: 9 - array[9]: 49
  UNIX>