Designing a Class

Read time: 29 minutes (7369 words)

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 new object.

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

main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//Copyright 2019 Roie R. Black
#include <iostream>
#include "PoolBall.h"

int main(void) {
    PoolBall ball;

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

Hopefully, you are certain this will work.

Here is a Makefile that will build our project. This pne is set up so you can tweak it to work on any system.

Makefile
 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
26
27
28
29
TARGET = poolballs
EXT =
PRE = ./
#PRE =
#EXT = .exe

RM = rm -f
#RM = del

SRCS = $(wildcard *.cpp)
OBJS = $(SRCS:.cpp=.o)

.PHONY: all
all:  $(TARGET)$(EXT)

$(TARGET)$(EXT):	$(OBJS)
	g++ -o $@ $^

%.o:	%.cpp
	g++ -c -std=c++11 -o $@ $<

.PHONY: run
run:	$(TARGET)$(EXT)
	$(PRE)$^

.PHONY:	clean
clean:
	$(RM) $(TARGET)$(EXT) $(OBJS)

This program obviously will not run until we set up our new class!

Setting up a Ball

We are going to design a simple class that manages pool balls. As a start, all we need to do is set up the class header file and set up one critical fact about the pool ball: 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:

PoolBall.h
1
2
3
4
5
6
7
// Copyright 2019 Roie R. Black
#pragma once

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

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:

main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//Copyright 2019 Roie R. Black
#include <iostream>
#include "PoolBall.h"

int main(void) {
    PoolBall ball;

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

And, here is our output:

$ make clean
rm -f poolballs main.o
$ make run
g++ -c -std=c++11 -o main.o main.cpp
g++ -o poolballs main.o
./poolballs
Testing the poolball class

All we have done so far is to set up C++ so it can create a container 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 keep 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 here.

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 parameter. 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:

#pragma once

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);
};

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 velocity? 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:

#pragma once

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

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.

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

#pragme once

class Ball: public Shape {

    public:
        void setRadius(Int r);

    private:
        int Radius;
};

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 orientation” starts to make thinking about your code a lot easier!