Designing a Class ################# .. include:: /header.inc .. vim:filetype=rst spell: 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: .. literalinclude:: code/main1.cxx :linenos: :caption: main.cpp 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. .. literalinclude:: code/Makefile :linenos: :caption: Makefile 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: .. literalinclude:: code/PoolBall.h :linenos: :caption: PoolBall.h 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: .. literalinclude:: code/main1.cxx :linenos: :caption: main.cpp And, here is our output: .. command-output:: make clean :cwd: code .. command-output:: make run :cwd: code 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: .. code-block:: c #include #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: .. code-block:: text 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: .. code-block:: c #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: .. code-block:: c #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: .. code-block:: c #include #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! .. code-block:: text 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! .. code-block:: c #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: .. code-block:: c 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: .. code-block:: c #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: .. code-block:: c #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: .. code-block:: c #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!