C++ Pointers

All of the work done in designing our Poolball simulation up to this time focused on the basic properties of the Poolball and Pooltable objects themselves. We need to figure out how to add a collection of Poolballs to a Pooltable.

Objects as Parameters

As we discussed earlier, actually passing a Poolball object to another object is a bit difficult to do. We can pass a copy of an object to another one, but the original object stays behind, under control of the originator of this action. C++ provides a way out of this problem by allowing us to pass a reference to an object to another method. Unfortunately, we cannot store that referenced object in an array, which is what we need to do in our Poolball simulation

Exactly what C++ does when we pass an object by reference is left to the compiler to figure out, and the C++ standard just says that we are not allowed to put a reference into an array.

Using References

When we pass a reference to another method as a reference, we do not use any special notation to use the object in the second method. We use it like any normal object, modifying the values as we need to. When the calling method gets control back, the changes will be felt in the original object.

If we try to make a copy of that variable (the one referring to the caller’s object) things get messy quickly: how is this new copy actually implemented. Is it a piece of memory holding all the original objects attributes, a copy of those, or a copy of a copy of those? Phew?

C++ Pointers

We get out of this mess, and introduce a huge new capability in our language, by introducing a new data type: the pointer.

A pointer is just a memory container containing the address of some object that occupies memory in the computer when your program runs. C++ requires that we identify exactly what this address points to. Once we know this, we can access that object indirectly. We cannot use the name of the object, since we will not know that when we receive just an address, so we use the name of the container we set up to hold the pointer, and some notation that tells the system to follow the pointer to find the real object to play with.

Here is what this looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class SimpleObj {
    public:
        int value;
};

int main() {
    cout << "Testing Pointers" << endl;

    SimpleObj obj1;
    SimpleObj * obj_ptr;

    obj1.value = 5;

    cout << "The value of obj1 is " << obj1.value << endl;

    obj_ptr = &obj1;
    obj_ptr -> value = 10;

    cout << "The value of obj1 is " << obj1.value << endl;
}


The critical new declaration is on line 13.

Read it like this:

obj_ptr is a pointer (*) to a SimpleObj object

Here is what you should get when running this code:

$ g++ -o demo test1.cpp
$ ./demo
Testing Pointers
The value of obj1 is 5
The value of obj1 is 10

The new notation is that “->” thing that was used to access the attribute of the object being pointed to by this new container, obj_ptr. (Notice how I name pointer variables. I do not want to be confused about what this thing is!)

In my past teaching, I came up with an analogy that helps explain what is going on in the C++ system.

Variables

When you first learned about programming, you were introduced to the idea of a variable, right?

A variable is a chunk of memory holding a bunch of bits. Pretend that this container is a physical box with a lid on it. On the side of the box, you write the name of the variable. When you declared this variable, the system manufactured this box for you using a blueprint we call a data type, and stuck the new box on a shelf somewhere in the system (in memory). Exactly where, you do not care (normally).

When you open the lid of the box, you usually see a bunch of switches, all set on or off at random. If you initialized your variable, the switches are set according to the encoding scheme used by the data type you used to describe the box when you first set it up. Got this so far?

Objects

An object is the same kind of thing, with one major difference. When we open up this box, we find a set of other boxes, each manufactured exactly the same way, then collected and put into this new object box. The inner boxes hold the object’s attribute variables. We must open them up to get at actual places to put information. The proper term for this object box is data structure, meaning a box that holds a collection of other things.

When we access the object box, we use the name written on the side of the box, something like obj. When we mean that we want to access one of the attribute inner boxes, we use notation like obj.value, meaning open up the object box, then open up the inner value attribute box.

Note

We use the same concept to access methods defined in the class that set up the object. The dot notation lets us access a method that will work on the attribute data of one specific object.

Pointers

A pointer variable is exactly the same thing as a simple variable, except for one new thing. When you open up the lid of this box and look inside, you do not find a bunch of switches. Instead, you find a hook. This new box is called a pointer variable, and the hook helps you find the thing it points to.

When you build a new pointer variable, the box is manufactured, put on a shelf somewhere, just as before. The hook has nothing hanging from it. It is uninitialized.

Warning

Just as with normal variables, we should explicitly make sure it does not point to some random thing in memory by making its value NULL, meaning “points to nothing”. NULL is defined as part of the “iostream” library.

If we place the address of a real object variable of the type we told the system we would be pointing to, the system goes to the box we want to point to and ties a piece of rope around that box. It ties the other end of the rope on the hook in the pointer variable box.

Now, when you open up the pointer variable box, you see a hook with a piece of rope tied on it. If you drag in that rope, the box you are pointing to will end up in your hands (well, not rally, it is actually glued to the shelf, so you will have to follow the rope to find that box.) Opening up the box you find will let you access that boxes switches.

Simple, and the analogy works pretty well at explaining how things work in C++ programs. The “->” notation tells the system to follow the rope to a box. When you find it, the box has a second box labeled “value” inside. Open up that one to proceed. Phew!

And Furthermore

Of course, funny things can happen when you open up the box at the end of the rope. What would happen if that box was actually an object? Simple, we just open it up and access the inner box. In our example, that box was an integer, and had switches we interpret as an integer number.

But what if when we opened up the inner box, we find a second hook? YIKES, what will we do now. Just follow the rope and see where it leads. We will see more of this in a later lecture. For now, let your imagination run wild on what you can set up and how you will find your way through this new tangle of ropes!

Passing Pointers to Methods

Once we have the idea of a pointer under control, we can pass an address to a method as a second way to access the caller’s object. Here is what it looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class SimpleObj {
    public:
        int value;
};

int obj_fetcher(SimpleObj * arg1) {
    return arg1->value + 5;
}

int main() {
    cout << "Testing Pointers" << endl;

    SimpleObj obj1;

    obj1.value = 5;

    cout << "The value of obj1 is " << obj1.value << endl;

    cout << "The obj_fetcher function returned " << obj_fetcher(&obj1) << endl;
}


And here is the output:

$ g++ -o demo test2.cpp
$ ./demo
Testing Pointers
The value of obj1 is 5
The obj_fetcher function returned 10

This version obviously accessed the object that was being pointed to, but can the new method actually modify the original one? Try this version:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class SimpleObj {
    public:
        int value;
};

void obj_modifier(SimpleObj * arg1) {
    arg1->value += 5;       // modify the original
}

int main() {
    cout << "Testing Pointers" << endl;

    SimpleObj obj1;

    obj1.value = 5;

    cout << "The value of obj1 is " << obj1.value << endl;
    obj_modifier(&obj1);
    cout << "The value of obj1 after the call to obj_modifier is " << obj1.value << endl;
}


Here it the final output:

$ g++ -o demo test3.cpp
$ ./demo
Testing Pointers
The value of obj1 is 5
The value of obj1 after the call to obj_modifier is 10

Looks like it can!