Lecture Notes for Chapter 8

James S. Plank


1. Pointers

The book has very nice text on pointers. The basic vibe is this: A variable such as an int consumes storage. You access that storage by using the variable. 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 on my (Dr. Plank's) Macintosh (different machines will print pointers differently):

  UNIX> g++ pex1.cpp
  UNIX> g++ -o pex1 pex1.cpp
  UNIX> pex1
  i      is: 5
  &i     is: 0xbffff748
  i_ptr  is: 0xbffff748
  *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, cu;

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

    square_and_cube(n, &sq, &cu);

    cout << n << " squared is " << sq << endl;
    cout << n << " cubed   is " << cu << 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 - JWM)

I can't stress this enough -- regardless of what the book says, C++ and C always 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

Remember const?
      Example:   const int COUNT = 100;
                        const int array[COUNT];


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 methods (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);
  
    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> 


6. Vectors make copies -- Arrays do not

Again, this is important stuff. When you pass a vector as an argument to a function, a copy of the entire vector is made. Look at pex9.cpp:


  #include <iostream>
  #include <vector>
  using namespace std;

  vector <int> change_and_return(vector <int> a)
  {
    int i;
  
    for (i = 0; i < a.size(); i++) a[i] = 30 + i;
    return a;
  }

  int main()
  {
    vector <int> a(10), b;
    int i;

    for (i = 0; i < a.size(); i++) a[i] = 10+i;

    b = change_and_return(a);

    for (i = 0; i < a.size(); i++) {
      cout << "i: " << i << " - a[" << i << "]: " << a[i] << 
                            " - b[" << i << "]: " << b[i] << endl;  
    }
  }

When you run it, you'll see that change_and_return() changes the contents of a, but that its version of a is a copy of the version in main(). So the version of a in main() remains unchanged when we print it out. Note also that when we return a vector, it makes a copy. So this program actually has three copies of 10-element vectors -- the original a in main, the copy of a in change_and_return(), and the return value that is put into b.

  UNIX> g++ -o pex9 pex9.cpp
  UNIX> pex9
  i: 0 - a[0]: 10 - b[0]: 30
  i: 1 - a[1]: 11 - b[1]: 31
  i: 2 - a[2]: 12 - b[2]: 32
  i: 3 - a[3]: 13 - b[3]: 33
  i: 4 - a[4]: 14 - b[4]: 34
  i: 5 - a[5]: 15 - b[5]: 35
  i: 6 - a[6]: 16 - b[6]: 36
  i: 7 - a[7]: 17 - b[7]: 37
  i: 8 - a[8]: 18 - b[8]: 38
  i: 9 - a[9]: 19 - b[9]: 39
  UNIX> 

This makes vectors convenient, because you can return them from procedures and you don't have to worry about overwriting stuff. However, it can be inefficient time-wise -- making copies of large vectors will consume time.

Now, look at the array version of the above code. In: pexA.cpp:


  #include <iostream>
  using namespace std;

  int *change_and_return(int *a)
  {
    int i;
  
    for (i = 0; i < 10; i++) a[i] = 30 + i;
    return a;
  }

  int main()
  {
    int a[10], *b;
    int i;

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

    b = change_and_return(a);

    for (i = 0; i < 10; i++) {
      cout << "i: " << i << " - a[" << i << "]: " << a[i] << 
                            " - b[" << i << "]: " << b[i] << endl;  
    }
  }

When you run it, you'll see that change_and_return() actually changes a in main(). That's because there is only one copy of the array -- the procedure call and return value simply pass around pointers:

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


7. A common and disastrous bug: Returning memory that has gone away

Look over the following program very carefully - pexB.cpp:


  #include <iostream>
  using namespace std;

  int *initialize_array()
  {
    int a[10];
    int *a_ptr;
    int i;

    for (i = 0; i < 10; i++) a[i] = 30+i;
    a_ptr = a;
    return a_ptr;
  }
  
  int main()
  {
    int *a;
    int i;

    a = initialize_array();

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

What it does is have create and initialize an array of ten elements, and then return a pointer to it. You'll see that when main() tries to print out the array, it's garbage. Why?

  UNIX> g++ -o pexB pexB.cpp
  UNIX> pexB
  i: 0 - a[0]: 30
  i: 1 - a[1]: 10
  i: 2 - a[2]: 1
  i: 3 - a[3]: 11597
  i: 4 - a[4]: -1599039200
  i: 5 - a[5]: -1599040224
  i: 6 - a[6]: -1073744104
  i: 7 - a[7]: -1867372625
  i: 8 - a[8]: -1610605544
  i: 9 - a[9]: 10
  UNIX> 

Well, because the memory for a in initialize_array() only exists while initialize_array() is running. When initialize_array() returns, the memory is freed up to be reused. And it does get reused in the cout statement.

This is a disastrous bug because it is hard to track down. Remember it, because it will happen to you someday.

The vector version of this code works fine - pexC.cpp:


  #include <iostream>
  #include <vector>
  using namespace std;

  vector <int> initialize_array()
  {
    vector <int> a(10);
    int i;

    for (i = 0; i < 10; i++) a[i] = 30+i;
    return a;
  }
  
  int main()
  {
    vector <int> a;
    int i;

    a = initialize_array();

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

This works fine because a copy of a is made and returned to main().

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


8. C-style strings, char *'s and c_str

C-style strings are character arrays that are NULL-terminated. This means that their last character is '\0', which is called the "NULL character." You can create a C-style string in many ways. The program pexD.cpp uses four ways -- using string constants (in double-quotes), using array initialization, writing code to create a string, and finally using the c_str() method of a C++ string. Note, they all need the NULL character at the end:


  #include <iostream>
  #include <cstring>
  using namespace std;

  int main()
  {
    char *s1 = "Jim";
    char s2[6] = { 'P', 'l', 'a', 'n', 'k', '\0' };
    char s3[4];
    const char *s4;
    string string4 = "Mayo";

    s3[0] = 'M';
    s3[1] = 's';
    s3[2] = '.';
    s3[3] = '\0';

    s4 = string4.c_str();

    cout << "The four strings: " << s1 << ", ";
    cout << s2 << ", " << s3 << ", " << s4 << endl;

    cout << endl;
    cout << "Strlen(s1) = " << strlen(s1);
    cout << ".  Strlen(s2) = " << strlen(s2);
    cout << ".  Strlen(s3) = " << strlen(s3);
    cout << ".  Strlen(s4) = " << strlen(s4) << "." << endl;

    cout << endl;
    cout << "Strcmp(s1, \"Jim\") = " << strcmp(s1, "Jim") << endl;
    cout << "Strcmp(s1, \"Fred\") = " << strcmp(s1, "Fred") << endl;
    cout << "Strcmp(s1, \"Plank\") = " << strcmp(s1, "Plank") << endl;  
    cout << "Strcmp(s1, \" Jim\") = " << strcmp(s1, " Jim") << endl;
    cout << "Strcmp(s1, \"jim\") = " << strcmp(s1, "jim") << endl;
    cout << "Strcmp(s1, \"Jix\") = " << strcmp(s1, "Jix") << endl;
 
    return 0;
  }

This creates four strings -- note, the middle two have to specify the memory; the first and last are simply pointers. If you use c_str() the pointer must be a const, which says that you cannot modify its contents.

The program also shows the use of strlen(), which returns the length of the string, minus the NULL character. Then it shows strcmp() which compares two strings lexicographically, and returns 0 if they are equal, a negative number if the first is less than the second, and a positive number if the first is greater than the second. Lexicographic comparison is done using the ASCII character codes for the characters. Since space is less than capital-J, the fourth strcmp() statement shows that " Jim" is less than "Jim".

  UNIX> g++ -o pexD pexD.cpp
  UNIX> pexD
  The four strings: Jim, Plank, Ms., Mayo

  Strlen(s1) = 3.  Strlen(s2) = 5.  Strlen(s3) = 3.  Strlen(s4) = 4.

  Strcmp(s1, "Jim") = 0
  Strcmp(s1, "Fred") = 4
  Strcmp(s1, "Plank") = -6
  Strcmp(s1, " Jim") = 42
  Strcmp(s1, "jim") = -32
  Strcmp(s1, "Jix") = -11
  UNIX>

Note that although it appears that strcmp() is returning the difference between the ASCII character codes of the differing characters, you should not rely on that, because the definition of strcmp() only specifies positive/negative numbers. This means that on a different machine, it may return different positive/negative values.


9. strchr(), strrchr() and strstr()

These are very useful functions on strings. Here are the prototypes:

  char *strchr(char *s, char c);
  char *strrchr(char *s, char c);
  char *strstr(char *s, char *tofind);
They all find something in the string s -- strchr() finds the first occurrence of c, and strrchr() finds the last occurrence of c. strstr() finds the first occurrence of the substring tofind.

If they find what they're looking for, they return a pointer to it inside s. If they don't, they return the global constant NULL.

Below is a nice example of it working (pexE.cpp):


  #include <iostream>
  #include <cstring>
  using namespace std;

  int main()
  {
    const char *s;
    string str;
    char *found;

    cout << "Enter a string: ";
    cin >> str;

    s = str.c_str();

    found = strchr(s, 'a');
    cout << "strchr(\"" << s << "\", 'a') returned ";
    if (found == NULL) {       // -OR- if (!found) ...
      cout << "NULL\n";
    } else {
      cout << '"' << found << '"' << endl;
    }

    found = strrchr(s, 'a');
    cout << "strrchr(\"" << s << "\", 'a') returned ";
    if (!found) {
      cout << "NULL\n";
    } else {
      cout << '"' << found << '"' << endl;
    }

    found = strstr(s, "ba");
    cout << "strstr(\"" << s << "\", \"ba\") returned ";  
    if (found == NULL) {
      cout << "NULL\n";
    } else {
      cout << '"' << found << '"' << endl;
    }
    return 0;
  }

This shows calling the various functions on a user-entered string:

  UNIX> pexE
  Enter a string: Jim
  strchr("Jim", 'a') returned NULL
  strrchr("Jim", 'a') returned NULL
  strstr("Jim", "ba") returned NULL
  UNIX> pexE
  Enter a string: Abacab
  strchr("Abacab", 'a') returned "acab"
  strrchr("Abacab", 'a') returned "ab"
  strstr("Abacab", "ba") returned "bacab"
  UNIX> 


10. Sizeof and const -- read the book