CS 218 - Object Oriented Programming with C++

Chapter 14 - Pointers, Classes, etc.

Objectives:

This chapter discusses pointers and how they relate to other concepts. Objectives important to this chapter:

  1. Pointer variables
  2. Declaring and using pointers
  3. Address of operator, dereferencing operator
  4. Pointers to a class or struct
  5. The new and delete operators
  6. Pointer math
  7. Dynamic arrays
  8. Shallow copy, deep copy
  9. Passing a pointer
Concepts:

The chapter begins with a recap of an earlier lesson: there are three kinds of C++ data types. All data types are either simple, structured, or pointer data types.

Students usually have difficulty with the concept of pointers. A pointer is a variable used to store an address.

Pointers are declared using the format

	int * ptr;
      

where ptr points to type int. Yes, kids, this is yet another use of the asterisk. This notation is flexible. Your text explains several features of it:

  • in the example "int * ptr" I have put a space before and after the asterisk. I could have left out either of those spaces, and the syntax would still be okay. (e.g. int* ptr or int *ptr)
  • in this example we are NOT declaring a variable of type int. We are declaring a pointer variable that points to a memory address that holds or will hold an int.
  • Whenever we declare a simple variable, we state its type (e.g. int, char, double). When we declare a pointer, we declare the type that it points to.
  • If we declare two pointers to a data type in the same statement, they both need asterisks:
    int *p1, *p2; // this declares two pointers to int variables
    int *p1, p2; // this declares p1 as a pointer to int, and p2 as an int variable
    For this reason, the text recommends always putting the asterisk by the variable name, instead of by the type name when declaring the variable.

To assign an address of a variable called pooh to a pointer variable called ptr, use the format

	ptr type = &pooh;
      

This assumes that ptr is a pointer, and that is was declared to point to the type of variable that pooh is. (A careless programmer could get in trouble here.) This also introduces the use of the ampersand, the address of operator. The line above assigns the address of the pooh variable to the ptr

To find the value stored in an address using the indirection operator. The what? Well, the indirection operator is the asterisk again. This time, it is used to indirectly access the contents of a variable. In the example so far, ptr points to the variable pooh. We use the indirection operator to read the value at the location that ptr points to, and assign it to the variable piglet:

	int piglet;
	piglet = *ptr;
      

Thus,

	ptr = &pooh;
	piglet = *ptr;
      

means the same as

	piglet = pooh;
      

Pointers can be used to pass the address of variables to functions. The function call:

	interchange( &x, &y);
      

passes the addresses of two variables to a function called interchange. That function might be defined as

	void interchange(int * u, int * v)
      

where u and v both point to type int. Note that the variables in the called function are different from the ones in the calling function. Since the variables are local to each function, the two sets could even have had the same names, and they would still be different variables. (Yes, you can take an aspirin now. I know I did.)

The text goes on to describe pointers to classes. If we can declare a variable that is an instance of a class (or a struct), we can also declare a variable that is a pointer to such a variable.

Assume a struct called studentType. We can create an object of this type and a pointer to that type as well:
studentType student; // declares a variable of this type
studentType *studentPtr; // declares a pointer to type studentType
studentPtr = &student; //assigns the address of student to the pointer studentPtr

Once again, note the three critical steps: we declared a variable, we declared a pointer to the variable's type, and we assigned the variable's address to the pointer.

The notation gets a little odd when we use the pointer to make a change to a member of the struct:
(*studentPtr).gpa = 3.9; // assigns the value 3.9 to the gpa member of student

The parentheses in the example above are necessary. The text explains that the dot operator has a higher precedence than the dereferencing operator (the asterisk), so we must use parentheses in this notation. Once again, in English: we need the computer to read that line and understand that we mean "the struct at the address that studentPtr points to", and its member variable called gpa will now hold 3.9. If we had left off the parentheses, the computer would have thought we meant the gpa member of studentPtr, which does not exist.

To avoid this problem, we can use a different notation: studentPtr -> gpa = 3.9;
This notation uses the member access operator arrow (designed by Oliver Queen, no doubt), the hyphen followed by the greater than symbol. This notation simplifies the reference. It means "assign 3.9 to the gpa member of the object at the location that studentPtr points to".

Pointers, like other variables, do not have values until values are assigned. It is allowed to assign either 0 or NULL to a pointer variable to initialize it.

The discussion so far has not told us why we would want to use pointers instead of the variables that they point to. The text reveals a new secret: you can create a pointer that points to a dynamic variable. A dynamic variable has a type, but does not have a name. It can be created and deleted when we are done with it. When it is created, a previously declared pointer should be assigned to it, which is what gives us access to the variable. Without the pointer, we have no access to the variable. Example:
int *p; // declares a pointer to int
p = new int; // creates a dynamic int, and assigns the address to p

This code uses the new operator to dynamically create an int variable, and to assign a pointer to it called p. To assign a value to this unnamed int, we use the *p notation:
*p = 25;
This means to store 25 as the value of the int that p points to.

To create an array dynamically, we could have written:
int *ptrIntArray;
char *ptrCharArray;

ptrIntArray = new int[20]; // creates a new array of ints and assigns a pointer to the start of the array
ptrCharArray = new char[30]; // creates a new array of chars and assigns a pointer to the start of the array


Correction: there was a typo above that has been removed
.int *ptrIntArray; // This line is correct, the asterisk is required when declaring the pointer
ptrIntArray = new int[20]; // This line is correct, the asterisk is not used when assigning the pointer
*ptrIntArray = new int[20]; // WRONG. Do not use an asterisk in this kind of statement.

Be aware that the new operator can fail if there is not enough memory available for use. It can create a bad_alloc (bad allocation command) exception, which will either be handled by the system or will crash the program.

Dynamic variables can be deleted from memory by using the delete operator and the pointer linked to the variable. Example:
delete p; // deletes the variable space that p points to, while p continues to exist
delete [] prtIntArray; // deletes (deallocates) the array that the pointer was associated with

Be aware that you are not really removing the stored data from memory. You are stating that the memory used by the dynamic variable(s) is now available for other uses.

Pointer facts:

  • Although you cannot assign a numeric value to a pointer (an actual address), you can assign the value of another pointer to it, providing they are the same kind of pointers:
    pointer1 = pointer2
  • You can test whether two pointers point to the same address: (pointer1 == pointer2)
    Note: a test uses a double equal sign, an assignment uses a single equal sign.

You can do integer math with pointers. Example:
int *ptrIntArray;
ptrIntArray = new int[20]; // ptrIntArray points to the first element of the array

To cause ptrIntArray to point to the next element, we could increment it: ptrIntArray++. Since ptrIntArray points to an int, adding one to it actually increments the address it holds by four bytes, since ints take up four bytes of memory space. Incrementing a pointer by an integer actually increments the address in holds by the value of the integer times the number of bytes it takes to hold the type it points to.

Be careful when doing math with pointers, since you can easily reassign the pointer to a memory location that you did not intend. This takes us into an area in which it is the programmer's responsibility to make sure the program does what is intended, not just what it is told.

As noted above, you can create a dynamic array, which can only be accessed by the pointer you assign to it. You can reassign the pointer to various elements of the array by incrementing, decrementing, adding integer values, or subtracting integer values. There is, however, another way.

Using the name of the pointer like the name of the array, you can use subscripts to access each element of the array. In the example above, ptrIntArray points to the first element of a dynamic array. So does ptrIntArray[0]. We could store a number (e.g. 5) in the second element with this:
*(ptrIntArray+1) = 5;
or we could do it like this:
ptrIntArray[1] = 5;

This is very much like what you have done with arrays previously, but there is a difference as well. If a standard array is declared with a type and a name, that name is like a pointer to the array and can be used with subscripts as we normally do. (And, as you have just learned, you can use a pointer to the array the same way.)

However, the name of the array cannot be incremented or decremented. It is considered a constant, and must always point to the first element of the array. The text calls it a constant pointer. A regular pointer is a variable, and it can be made to point to any element of the array by math operations on it.

One further oddity: since the name of an array is like a pointer to the array, you do not use the address of operator with it when you assign a pointer to it.

Examples:
int score[20]; // declares an array of ints, size 20
int *scoreptr; // declares a pointer to int
score[1] = 14; // assigns the value 14 to the second element of the array
scoreptr = score; // since score is a pointer to the array, it holds an address, so we do not need to use the address of operator to assign that address to scoreptr
scoreptr[1] = 21; // changes the value of the second array element

The text turns briefly to creating an array sized by the user. This is not possible with the array lessons from earlier chapters, but dynamic arrays allow you to ask the user for a size, store it in an int, and use the new command to create an array of the requested size. Example:

int usersize; // creates an int to store the user's requested size
int *ptrUserArray; // creates a pointer to access the array we will make
cout << "Enter the size for your array: "; // asks the user for the size
cin >> usersize; // stores the input in the variable
cout << endl; // moves the cursor to a new line
ptrUserArray = new int[usersize]; // creates the array sized as needed, and assigns its address to the pointer

In the example above, we created an array of integers. It could just as easily been an array of any other data type.

The text introduces two new phrases: shallow copy and deep copy. (Silly names, but easy to understand.)

  • A shallow copy exists if you have two or more pointers that point to the same memory (either a single variable or an array). This is a problem because if you delete the memory (reallocate it to the memory pool) using one of the pointers, you actually make all such pointers point to invalid locations. You may need shallow copies in some programs, but you must be aware of this danger.
  • A deep copy exists if each pointer points to unique memory locations. This is a better situation, in most cases.

Pointers can be passed by reference or by value to functions. In the definition of the function, you either use an ampersand (to make it a reference) or don't (to make it a value). Examples:

void function1 (int* &ptr) // the ampersand tells the function to receive a reference to a pointer
{
body of the function
}

void function2 (int* ptr) // no ampersand tells the function to receive the value of the pointer
{
body of the function
}