Beyond vectors

The first step on the road to object-oriented programming came when we realized that vectors weren't all they were cracked up to be. They certainly could collect together large amounts of data under a single variable, but all the data were restricted to be of the same type. In the real world (the place programs often try to imitate or describe), a single entity often is described by information of many different types. A student has a name (string), a gpa (double), a student id (long), a gender (char), and his/her enrollment status (bool).

Some would say: "Just use a set of parallel vectors!" Sure, we could do that, but working with parallel vectors is cumbersome when the number of vectors becomes moderate to large. And if you are thinking of sorting them...well, don't. (When two items are out of order, you must swap their information in all of the parallel vectors -- not just the one whose value you are using in the sort comparisons.)

And this argument can apply even in simple cases like a 2D point. Such an entity consists of simply two real values (two doubles). This would simply be a pair of parallel vectors. Yet dealing with a collection of 2D points could still be a harrowing experience.

To alleviate some of this strain, C introduced the struct mechanism. A structure can collect together multiple data values of (possibly) different types. This allowed simplifications of coding situations and even added some amount of power.

Take, for instance, this set of 2D point functions:

    void input(double & x, double & y);
    void print(double x, double y);
    double distance(double x1, double y1, double x2, double y2);
    void midpoint(double x1, double y1, double x2, double y2,
                  double & xm, double & ym);

If we were to use this struct:

    struct Point2D
    {
        double x, y;
    };

We could simplify the functions' look considerably:

    Point2D input(void);
    void print(Point2D p);
    double distance(Point2D p1, Point2D p2);
    Point2D midpoint(Point2D p1, Point2D p2);

Notice how the structure type is used for both value arguments and return values! Since a Point2D struct consists of both x and y values, these new versions have all the same information as the previous ones. They are more compact, however, because it only requires one variable to describe a single 2D point instead of two.

If you had hold of a Point2D variable/object, you could manipulate its x and y values with the dot (.) operator:

    Point2D input(void)
    {
        char t;
        Point2D p;
        cin >> t >> p.x >> t >> p.y >> t;
        return p;
    }

    void print(Point2D p)
    {
        cout << '(' << p.x << ", " << p.y << ')';
        return;
    }

This was a major step forward from the simple variable. And since the struct is collects the multiple data values of (possibly) different types, it can simplify those situations that once led us down the path of the parallel vector! (Just make a single vector of the struct type and it will have all the data collected together in a single element.)

There are a few desirable features still missing, however. So let's try to improve on the struct.

Basic Object-Orientation

C++ introduced the class to improve on the struct. The idea here is that a class data type is describing an entire class of entities. For instance, all 2D points have both an x and a y coordinate. So a class for 2D points would contain data members to represent these values. However, unlike in a struct, access to the data members of the class would be restricted to qualified/secured personnel only. In fact, only those functions intimately related to the class would be allowed direct access to the data values contained there-in. To do this, we had to place the functions that would operate on 2D point data inside the class. This might look something like this:

    class Point2D
    {
        double x, y;
        Point2D input(void);
        void print(Point2D p);
        double distance(Point2D p1, Point2D p2);
        Point2D midpoint(Point2D p1, Point2D p2);
    };

(Note that this class and the above Point2D struct could not exist in the same program. As usual, we are illustrating a progression toward a final 'best' version of the code.)

Two things need adjustment with this view, however. First, since the data are private (restricted) by default, we need to distinguish the data from the functions. (If the functions were restricted, no one could do anything with this class!) We show the distinction with the keyword public:

    class Point2D
    {
        double x, y;
    public:
        Point2D input(void);
        void print(Point2D p);
        double distance(Point2D p1, Point2D p2);
        Point2D midpoint(Point2D p1, Point2D p2);
    };

Now the functions can be accessed from outside the class while the data remain private. (In fact, we could state this more explicitly by using the keyword private before the data member declarations, but I like to leave it off because it is the default and not needed.)

The second 'problem' is that the functions now have too much information coming in and/or going out. Since we are allowing these functions inside the class to have direct access to the data, they will automatically have an x and a y when they execute. Therefore, they needn't have as many arguments. We can stream-line them like so:

    class Point2D
    {
        double x, y;
    public:
        void input(void);
        void print(void);
        double distance(Point2D p2);
        Point2D midpoint(Point2D p2);
    };

This looks a bit odd at first, so let's look at how a class would be used elsewhere in the program.

    Point2D start, end, half;

    cout << "Where will we begin?  ";
    start.input();
    cout << "And where will it all end?  ";
    end.input();

    half = start.midpoint(end);  // could've been end.midpoint(start), too

    half.print();
    cout << " is half-way along our journey from ";
    start.print();
    cout << "\nto ";
    end.print();
    cout << ".\n";

Here we see a simple program to determine the half-way point of a journey. The starting and ending coordinates are read in, processed, and then the whole thing is printed out (nicely as always).

When you want to call a class member function (also called a class method or simply method), you place a variable of that class in front of the function's call separated by a dot (.) operator. We've seen this syntax before with several functions owned by cin and cout. So you shouldn't be too uncomfortable with the syntax of the situation. (Incidentally, it also means that cin and cout must necessarily be variables of some class types as well. So, entertainingly, you've been using object-oriented programming since your first "Hello World" program!)

So, when the program executes start.input();, it is saying to call the input() function from the Point2D class using the start variable of that class. As the input() function executes, it will use the start variable's x and y values as the place to store what the user types. (We'll see this in a minute.)

(Just a side note on vocabulary: variables declared of a class type are also called instantiations of the class, objects of the class, or simply objects.)

Let's look at how the above program fragment executes in some detail. First we declare 3 Point2D type objects: start, end, and half.

    start                 end                   half
    +------------+        +------------+        +------------+
    |   +------+ |        |   +------+ |        |   +------+ |
    | x |      | |        | x |      | |        | x |      | |
    |   +------+ |        |   +------+ |        |   +------+ |
    |            |        |            |        |            |
    |   +------+ |        |   +------+ |        |   +------+ |
    | y |      | |        | y |      | |        | y |      | |
    |   +------+ |        |   +------+ |        |   +------+ |
    +------------+        +------------+        +------------+

Notice how each object contains sub-variables named x and y. Next we print a prompt on the screen:

    Where will we begin?  _

From here the user types their first point:

    Where will we begin?  (8.2,-4)
    _

This is read inside the input() call from the start object. So, now our variables look like this:

    start                 end                   half
    +------------+        +------------+        +------------+
    |   +------+ |        |   +------+ |        |   +------+ |
    | x |  8.2 | |        | x |      | |        | x |      | |
    |   +------+ |        |   +------+ |        |   +------+ |
    |            |        |            |        |            |
    |   +------+ |        |   +------+ |        |   +------+ |
    | y | -4.0 | |        | y |      | |        | y |      | |
    |   +------+ |        |   +------+ |        |   +------+ |
    +------------+        +------------+        +------------+

Next we prompt the user for the end-point:

    Where will we begin?  (8.2,-4)
    Where will it all end?  _

And they respond:

    Where will we begin?  (8.2,-4)
    Where will it all end?  (-6,9.8)
    _

This last entry is done inside the input() function as called by the end object. That makes our variables look like:

    start                 end                   half
    +------------+        +------------+        +------------+
    |   +------+ |        |   +------+ |        |   +------+ |
    | x |  8.2 | |        | x | -6.0 | |        | x |      | |
    |   +------+ |        |   +------+ |        |   +------+ |
    |            |        |            |        |            |
    |   +------+ |        |   +------+ |        |   +------+ |
    | y | -4.0 | |        | y |  9.8 | |        | y |      | |
    |   +------+ |        |   +------+ |        |   +------+ |
    +------------+        +------------+        +------------+

Next we call the midpoint() function from the start object and passing it the end object as the single (input) argument. It finds the midpoint (which is (1.1,2.9)) and returns it through the return mechanism. This result is then assigned into the half object. (Note that this means we CAN use the assignment (=) operator with class type variables (objects)!) That makes our variables:

    start                 end                   half
    +------------+        +------------+        +------------+
    |   +------+ |        |   +------+ |        |   +------+ |
    | x |  8.2 | |        | x | -6.0 | |        | x |  1.1 | |
    |   +------+ |        |   +------+ |        |   +------+ |
    |            |        |            |        |            |
    |   +------+ |        |   +------+ |        |   +------+ |
    | y | -4.0 | |        | y |  9.8 | |        | y |  2.9 | |
    |   +------+ |        |   +------+ |        |   +------+ |
    +------------+        +------------+        +------------+

Finally, we print() the half object:

    Where will we begin?  (8.2,-4)
    Where will it all end?  (-6,9.8)
    (1.1, 2.9)_

A little verbage:

    Where will we begin?  (8.2,-4)
    Where will it all end?  (-6,9.8)
    (1.1, 2.9) is half-way along our journey from _

Then print() the start object:

    Where will we begin?  (8.2,-4)
    Where will it all end?  (-6,9.8)
    (1.1, 2.9) is half-way along our journey from (8.2, -4)_

More verbage:

    Where will we begin?  (8.2,-4)
    Where will it all end?  (-6,9.8)
    (1.1, 2.9) is half-way along our journey from (8.2, -4)
    to _

And finally print() the end object (and the period on the sentence):

    Where will we begin?  (8.2,-4)
    Where will it all end?  (-6,9.8)
    (1.1, 2.9) is half-way along our journey from (8.2, -4)
    to (-6, 9.8).
    _

Interesting. But, without knowing how the functions themselves are implemented, it seems kinda hollow. So, on to the defining of class methods. Let's start with the input() method:

    void Point2D::input(void)
    {
        char t;
        cin >> t >> x >> t >> y >> t;
        return;
    }

Note that we have a Point2D:: in front of our function name. This is to remind the compiler that the input function we are defining is really a method of the Point2D class. The :: operator is known as the scope resolution operator. It is telling the compiler that the input function being defined is inside the scope of the Point2D class. (Recall our previous use of the :: operator to tell the compiler that certain functions or constants came from the std namespace while inside a library's interface file.)

Also note that we can still declare local variables inside class methods -- they are normal functions other than the fact that they are inside a class' scope. And it is that fact which brings us to the next point (sorry for the pun...couldn't resist): where are those x and y variables it is referring to? They are the data members of the Point2D class! Since this function is really a class method, it has (direct) access to that class' data members. Which x and y it uses depends on which object of the class actually called the method. In our program fragment above, there were two calls to the input() method: one from the start object and one from the end object. So, as the first call executes, the x and y referred to inside the input() method will be those inside the start object (just as we executed it above). During the second call, the x and y will refer to those inside the end object.

This tells us several things.

But let's look at another method. Here's the midpoint method:

    Point2D Point2D::midpoint(Point2D p2)
    {
        Point2D mid;
        mid.x = (x+p2.x)/2;
        mid.y = (y+p2.y)/2;
        return mid;
    }

This method brings up another point (sorry...I just keep doing it) about using objects. We'd said before that functions inside the class (methods) gain direct access to data members of the class. That's why we could just use x and y in the Point2D methods as if they'd been arguments or locals. They were members of the calling object -- in which the method is executing. But, this direct access isn't limited to the calling object. As a class method, we can directly access the data members of any object of this class!

In the midpoint() method we use this fact to retrieve the x and y values from the p2 argument and to store x and y values into the mid local variable.

The implementation of the print() and distance() methods is left as an exercise for the reader. *smile*

What if I want to do more than that?

The Point2D class so far offers storage of 2D point data and 4 operations on that data: input, print, distance, and midpoint. But there might be things that programmers using this class would want to do that we have not forseen (or do not wish to implement). For instance, a programmer doing a graphical application may want to draw a dot on-screen at a position described by a Point2D object. (This would be immediately useful in a scatter-plot for statistics or analysis -- see spreadsheet plots -- or to create larger structures when drawing many points together -- like lines, squares, circles, etc.) Although we could have forseen this, we do not want to support it. Drawing graphics -- even a single pixel (picture element -- those dots you see when you look really closely at your screen) -- is not for the weak at heart or the novice (often one in the same). (Graphics are also not portable between systems. A graphics program written for WinXP won't run on Win98. One written for Win98 won't run on Win3.1. One written for Windows won't run on Linux. One written on Linux won't run on MacOS 9.x. Etc.)

For people who want to do odd things like these, we offer a secondary interface to their data: accessors and mutators. The purpose of an accessor method is to allow the programmer using the class to access (retrieve a copy of) the data values inside an object. The graphics example above might involve something like this:

   Point2D position;
   draw_on_screen(position.get_x(), position.get_y());

Here the get_x() and get_y() are the accessors for the Point2D class. (The draw_on_screen() function would be a system-specific graphics function.) These functions are named from the perspective of the programmer using the class: that programmer wants to retrieve (i.e. get) a value from the object. By using two accessors for two data members (one for each), we are allowing individual access to the data. (If the programmer doesn't want all the data, they don't have to get it all. They can access just as much as they need.)

Let's say that in the same graphics program the programmer has retrieved mouse click coordinates and wants to store them in a Point2D object. This would involve changing (mutating) the values inside the object and so calls for mutators. It might look like this:

    Point2D clicked;
    double mouse_x, mouse_y;

    get_mouse_click_coord(mouse_x, mouse_y);
    clicked.set_x(mouse_x);
    clicked.set_y(mouse_y);

The set_x() and set_y() are the mutators. Again, they are named from the perspective of the programmer using the class who wishes to place (set) new values into the object. We give one mutator for each data member so that s/he can change just what they need/want to change without having to change all data at once. (The get_mouse_click_coord() function would be another system-specific function that we won't deal with further.)

Let's see how these functions would fit in with the class:

    class Point2D
    {
        double x, y;
    public:
        void input(void);
        void print(void);
        double distance(Point2D p2);
        Point2D midpoint(Point2D p2);
        double get_x(void);
        double get_y(void);
        void set_x(double new_x);
        void set_y(double new_y);
    };

And we've already seen their use, so on to their definition:

    double Point2D::get_y(void)
    {
        return y;
    }

    void Point2D::set_x(double new_x)
    {
        x = new_x;
        return;
    }

The opposite accessor/mutator are again left as a reader exercise.

If we wanted to allow the other programmers to change the data, why go through all the private/public non-sense in the first place. Why not just add functions to struct's and let them change what they want any time?!

One at a time. First: Why the mutators? Programmers are human. Humans make mistakes -- lots of them. Many programs have crashed because a programmer made a simple mistake like placing a 10 into a variable when they should have placed 100 (or maybe -10 or 20). Such clumsy mistakes can cause lots of damage to computer data and that can lose lots of money for a company or lots of progress for a research project. By forcing the programmer to go through a mutator to make changes it serves two purposes. First, the mutator is more typing than a simple assignment and so takes some thought. Perhaps s/he will think more about what it being changed and not make the simple typographic error mentioned above. ...er...maybe. Second, the class programmer can place error checking/validating code inside the mutator to dis-allow potential mistakes. If a class were representing the time of day, for instance, it would be anywhere from annoying to tragic for a minute value to be outside the range of 0 to 59 (inclusive). So, the mutator could check for such conditions and not allow a change that would violate the good range of values.

It wouldn't be bad to allow the programmer to access the data at any time because they can't harm it in any way (accidental though it may have been). But, it is terribly difficult to teach C++ that 'read' access is okay while 'write' access is not allowed. So, the private section(s) of a class restrict both types of access -- much easier. That's why we need the accessors.

Although our Point2D class doesn't have such illegal value problems (any double value is a legal coordinate -- the number lines go from negative infinity to positive infinity after all!), we should still practice using the mutators effectively. With that in mind, here is a revision of the input and midpoint methods from before:

    void Point2D::input(void)
    {
        char t;
        cin >> t >> x >> t >> y >> t;
        set_x(x);
        set_y(y);
        return;
    }

    Point2D Point2D::midpoint(Point2D p2)
    {
        Point2D mid;
        mid.set_x((x+p2.x)/2);
        mid.set_y((y+p2.y)/2);
        return mid;
    }

Notice that we use mutators to store values into our data members. This allows any error checking that the mutators do to be coded just once -- in the mutators -- and the rest of the class methods simply reuse it by calling the mutators as needed. (Also note that the input() method uses the member variables in the cin extraction and then again as arguments to the set_?() methods. This is a 'clever' idea to avoid extra local variables. Since you'll be over-writing the data in the mutator anyway, we over-write it with the user's attempt at data before sending that attempt into the mutator. Since the mutator accepts a value argument, its formal argument is a copy of this value and so won't affect the assignment process.

One Last Hurrah!

Let's do one more bit of practice using our new Point2D class before we move on to some other, more powerful features of classes. Let's create a small program using the Point2D type. Programs using a class place the class definition at the top of the file (up with function prototypes and #include's). Method definitions normally are placed after the main with all the other functions' definitions.

So what do we want this program to accomplish? Let's read in a point from the user to start. Then we'll create a second point which is 5 to the left and 3.5 down from the one s/he entered. Finally we'll print out both points and the distance between them. (See if you can guess what the distance will be before you run the program. Does it matter what point the user types in?)

    #include <iostream>
    using namespace std;

    class Point2D
    {
        double x, y;
    public:
        void input(void);
        void print(void);
        double distance(Point2D p2);
        Point2D midpoint(Point2D p2);
        double get_x(void);
        double get_y(void);
        void set_x(double new_x);
        void set_y(double new_y);
    };

    int main(void)
    {
        Point2D users, ours;

        cout << "Where are you?  ";
        users.input();

        ours.set_x( users.get_x() - 5 );    // 5 left
        ours.set_y( users.get_y() - 3.5 );  // 3.5 down

        cout << "Wow!  Since you are at ";
        users.print();
        cout << " and I'm at ";
        ours.print();
        cout << "\nwe're " << ours.distance(users)
             << " away from each other!\n";

        return 0;
    }

    void Point2D::input(void)
    {
        char t;
        cin >> t >> x >> t >> y >> t;
        set_x(x);
        set_y(y);
        return;
    }

    Point2D Point2D::midpoint(Point2D p2)
    {
        Point2D mid;
        mid.set_x((x+p2.x)/2);
        mid.set_y((y+p2.y)/2);
        return mid;
    }

    double Point2D::get_y(void)
    {
        return y;
    }

    void Point2D::set_x(double new_x)
    {
        x = new_x;
        return;
    }

    // the rest were exercises, remember?  *grin*

Notice how we used one function call in an expression whose result is passed into a second function as input. We've done this before, but sometimes people freak a little when they see it done with the dot (.) operator and objects' methods.