Designing a Class

When we start off building a blueprint for a new kind of object, we have some basic structure to construct. In keeping with the “baby steps” approach to building code, let’s set up a simple program that exercises our bew object.

Here is a simple main.cpp program that does not do much:

#include <iostream>

int main(int argc, char ** argv) {
   std::cout << "Testing the poolball class" << std::endl;
}

Warning

From this point on, I will be using the preferred C++ convention that says we will not be using the “using namespace” line. If you wish oto use it, you are on your own if you get into naming issues. YMMV!

Hopefully, you are certain this will work.

Setting up a Ball

We are going to design a simple class that manages poolballs. As a start, all we need to do is set up the class header file and set up one critical fact about the poolball: define the radius of the ball! Since, ultimately, we are going to be generating some graphics code to display the ball, I will make the radius an integer variable:

#ifndef POOLBALL_H
#define POOLBALL_H

class PoolBall {
    public:
        int radius = 5;
};

#endif

If we now modify our main program code, we can see that this new data type can be used to create a ball object, and display a fact about it:

#include <iostream>

#include "poolball.h"

int main(int argc, char ** argv) {
    PoolBall ball;

    std::cout << "Testing the poolball class" << std::endl;
    std::cout << "We have a new ball of radius " << ball.radius << std::endl;
}

And, here is our utput:

$ poolballs
Testing the poolball class
We have a new ball of radius 5

All we have done so far is to set up C++ so it can create a contianer to hold information about each PoolBall object we create. The actual creation is done in a data declaration statement, identical to those you have used to set up standard data types in your code.

Notice the new notation we use to get at that radius value. The object name followed by a dot and another name accesses attributes within the object container. In this example, we are allowing the outside world to access this value. Here is the result of that:

#include <iostream>

#include "poolball.h"

int main(int argc, char ** argv) {
    PoolBall ball;

    std::cout << "Testing the poolball class" << std::endl;
    ball.radius = 10;
    std::cout << "We have a new ball of radius " << ball.radius << std::endl;
}

Now, we see this:

Testing the poolball class
We have a new ball of radius 10

If this is not what you want, you can keet this from happening by restricting access to this particular attribute. However, in doing so, we also prevent getting access to the radius of the ball, unless we provide a method that can access that attribute. That needs a few changes.

Controlling Access to Object Attributes

Here is the change we need in our header file:

#ifndef POOLBALL_H
#define POOLBALL_H

class PoolBall {
    private:
        int radius = 5;
    public:
        int getRadius(void);
};

#endif

Now, we have set up a method that can get at the radius, but no one outside of the object itself can modify that attribute.

To make this work, we need to add code to implement the new behavoir we have added to this class:

#include "PoolBall.h"

int PoolBall::getRadius(void) {
    return radius;
}

Notice how we name this routine. The name of the class followed by two colons, and then the name of the method itself. Formally, the class name sets up something called a namespace (sound familiar?) that hides away all variables you use in code for this class. You access things through the objects, not directly!

Here is how we access the radius now:

#include <iostream>

#include "poolball.h"

int main(int argc, char ** argv) {
    PoolBall ball;

    std::cout << "Testing the poolball class" << std::endl;
    std::cout << "We have a new ball of radius " << ball.getRadius() << std::endl;
}

Attempting to access the attribute directly will not even compile!

poolball.h: In function 'int main(int, char**)':
poolball.h:6:22: error: 'int PoolBall::radius' is private
main.cpp:10:10: error: within this context
         ball.radius = 10;
      ^
In file included from main.cpp:4:0:
poolball.h:6:22: error: 'int PoolBall::radius' is private
         int radius = 5;

Constructors

Although we did not build one, C++ requires a special method called a constructor that tells it how to set up the new objects with this blueprint. If we do not provide one, the compiler will generate a default one for us, as it did in this case. The default routine allows no parameters, but you could write one if you had special work to do to get your object ready to go. We do not need that ehre.

Normally, you might want to set things up yourself with a bit more control, so you add a constructor to your class. The name of the constructor is the same as the name of the class iteslf!

#ifndef POOLBALL_H
#define POOLBALL_H

class PoolBall {
    private:
        int radius = 5;
    public:
        void PoolBall(void);        // constructor
        void PoolBall(int radius);  // constructor with initial radius
        int getRadius(void);        // private attrbiute accessor
};

#endif

Since we have already see that the default constructor works fine, there is no need to add one with no parameters. But, we might like to set up new balls with some other radius. In this case, we added a constructor that takes a single parameter which will be our new radius!

If we add our own constructor, the compiler will not build a default one for us, se we need to add our own for that first object. We do not need any code since there is nothing to do, so the implementation will be empty.

Here are the changes we need in the implementation file:

void PoolBall::PoolBall(int radius) {
    this->radius = radius;
}

Do not let that last line confuse you. The this->radius notation on the left hand side of the assignment statement refers to the object attribute, the radius on the right hand side refers to the paraeter. We need to tell the compiler explicitly which name is which using that funny this notation.

Constructors can be added to do different things when new objects are created. The only rule is that the signature, formally, the return type of the method, the method name, and the number and type of all parameters must be unique. You are doing something called overloading a name, and the compiler needs to be able to figure out which name you are referring to when there are more than one available.

Adding more Attributes

Our PoolBall class is not very useful for the intended application. We need to be able to tell our graphics engine where the ball is at any moment in time, and we need a way to track the velocity of the ball as it moves. We will use simple variables for all of these, and add code to set up a ball with an initial state. Here is a possibility:

#ifndef POOLBALL_H
#define POOLBALL_H

class PoolBall {
    private:
        int radius = 5;
        int xPos, yPos, xVel, yVel;
    public:
        PoolBall(void);
        PoolBall(int radius);
        PoolBall(int radius, int xPos, int yPos, int xVel, int yVel);
        int getRadius(void);
};

#endif

As we add code, we should be asking questions about what we have, and whether it still makes sense. Since we have added new attributes to our object, we must make sure we set things up properly. Does it make sense to set up a new ball and not specify where it is to ba placed on our table, and should we give it an initial valocity? The answers to these (and other) questions will cause you to alter your code. Personally, I do not like the first two constructors, so I would probably delete them and require a complete setup of each ball I want to have in my simulation. YMMV!

Inheritance

This is a bit of an advanced topic, but if we keep it simple, I think you can see how useful this can be.

Often one kind of object is similar to another kind of object you might work on. Suppose you have two classes and you discover that they both have attributes in common, but one of those classes has a few extra attributes. We can create a top lever class with the shared attributes, then “specialize” that lower level class by adding the additional attributes. We are setting up a “parent->child” kind of relationship.

Of course, this makes no sense unless the behaviours of the two classes are similar as well.

In examining our example class so far, we have focused on a single shape, a “ball” that shape has several attributes that define where the ball is located on the screen, and how fast it is moving. It also has attributes that describe the ball itself.

Now suppose we want to add another object, this one a rectangle to our program. Do we start from scratch and build an entirely new class?

We could, but let’s consider which attributes (and methods) we have designed that would need to also be in the new Rectangle class:

We still need data to tell us where some reference point in our shape is located. For the ball, the center of the object made sense. For a rectangle, it might be the center, but it also might be one corner. Instead of a radius, we need a width and height. The box will also have velocity attributes.

Here is a new class that collect the common attributes we want both objects to share:

#ifndef SHAPE_H
#define SHAPE_H

class Shape {
    public:
        void setX(int x);
        void setY(int y)
    protected:
       int Xpos;
       int Ypos
};
#endif

Notice that we have declared the shared attributes as “protected”, not “private”. This will allow classes that inherit these attributes to use those attributes as though they were private.

Her is how we create our Ball class, not inheriting from the new parent class:

#ifndef BALL_P
#define BALL_P

class Ball: public Shape {

    public:
        void setRadius(Int r);

    private:
        int Radius;
};
#endif

In this new specification, we only add the routines needed for this new kind of object. We are “specializing the Shape class and adding (never subtracting) features.

Phew, that is enough to make your head hurt! Fortunately, with practice, all of this “object orietation” starts to mke thinking about your code a lot easier!