CSCI 240 Lecture Notes Part 12 Object-Oriented Programming
Text of a C++ program illustrating the code developed in this document.
Object-Oriented Programming (OOP) is the next step in modern programming practice after functional programming (60s) and then structured programming (70s).
It does not replace the principles learned so far in the course, rather it builds upon them.
OOP is complex. The first of three main principles of OOP is that of the class. Programming with classes is the basis of OOP.
The idea of a class is simple: a class is a kind of a package that consists of (functions plus the data they use).
However, in OOP, functions are called methods.
And individual variables defined in a class are called data members.
Each instance of a class contains data and methods that represent a single thing, or object (an Employee, an Account, a Line, a Car, etc.).
So a class is a template for the creation of multiple instances of that class, each of which is called an object.
A class is really a new data type.
To illustrate the new ideas, lets start with a C++ program that does not use object-oriented techniques.
Recall how we could draw a line with the graphics function line(screen, x1, y1, x2, y2, color)
Version 1: A program to draw a line:
A C++ non object-oriented program to draw a line (omitting graphics init)int main()
{
int x1, y1, x2, y2;
x1 = 12; y1 = 23; x2 = 34; y2 = 45;
line(screen, x1, y1, x2, y2, 50); return 0; } |
Note that to draw 1 line we need
To add more drawable lines to the program, we need
Also, we need a way to keep track of all the coordinates maybe a naming convention like L1x1 (for Line1, x1) etc. If we dont voluntarily adopt a good naming system, we will likely get very confused with a lot of lines.
Version 2: To simplify this somewhat, lets put the x's and y's in a structure:
struct TLine
{
int x1, y1, x2, y2;
};
Now the program could look like this:
C++ program to draw a line using a TLine struct:int main()
{
TLine L;
L.x1 = 12; L.y1 = 23; L.x2 = 34; L.y2 = 45; line(screen, L.x1, L.y1, L.x2, L.y2, 50); return 0; } |
but we can improve on this by writing a function:
void drawLn(TLine l)
{
line(screen, l.x1, l.y1, l.x2. l.y2, 50);
}
Then the the main() program to draw the line in the program becomes:
C++ non object-oriented program to draw a line using a struct and helper function:int main()
{
TLine L;
L.x1 = 12; L.y1 = 23; L.x2 = 34; L.y2 = 45; drawLn(L); return 0; } |
So in version 2, we have
To add another line, we need only
Note that when we want to draw the line, we dont need to recall the names of its 4 int data members just the name of the line (which is the argument to the function drawLn()).
Version 3 we make this object oriented by creating a class called Line, which includes not only the variables that represent the line, but also the code that draws it. The class definition is a little like a (struct + some function prototypes). Heres what the class looks like (with one detail to be added later):
C++ Line Class Definition:class Line
{
int x1, y1, x2, y2; // the class variables
Line(int, int, int, int); // the "constructor" void drawLn(); // the draw method }; // note ; |
Now we must write code for the "constructor" and the drawLn() method:
C++ Code for Line, written outside the Line class definition:Line::Line(int left, int top, int right, int bottom)
{
x1 = left; y1 = top;
x2 = right; y2 = bottom;
}
void Line::drawLn()
{
line(screen, x1, y1, x2, y2, 50);
}
|
Now before we get to a program that uses these, a few notes:
- The "constructor" is the method called when you create an instance of a Line. We will see how to create an instance of a Line shortly. Here, its arguments are stored into the data members of the object. So it is a kind of special initialization method.
- The name of the constructor is always the same as the name of the class.
- The notations of Line:: in front of the constructor and the method are used to specify what class the methods belong to, since the code for the methods are not defined inside the class definition. It would be possible to have some other class with a drawLn() method, for example. It would be defined as OtherClass::drawLn(). This way the compiler can tell which class a given method definition belongs to.
Now that the Line class is defined, we can write a program that uses it. Just type in the class definition before main() (the actual methods can go before or after main()).
C++ Program to draw a line, using the Line class:int main()
{
// creates line by calling constructor
Line L = Line(12, 23, 34, 45);
L.drawLn(); return 0; } |
Thats it. Clearly, theres some work up front to design and define the Line class, but once thats done, the program becomes simpler.
Note that to add another Line, we need
Notice, too, the way the method is called its just like the structure dot notation in this case, the objectname.methodname(args if any).
Also, notice that the call to drawLn() does not need any arguments! The Line L "knows" its xs and ys (they are data members of L). So since the code in drawLn() (used by L) can "see" the data members (which are inside L), no arguments are needed.
Its like main() is telling the line L to draw itself. In fact, in OOP, calling a method of an object is called "sending a message to the object". In this case, its the "draw yourself" message.
Another thing to understand clearly: each object of the Line class has its own set of variables (data members). So calling the drawLn() method for different Line objects will cause different lines to be drawn:
C++ Program to draw 2 linesvoid main()
{
Line L1 = Line(12, 23, 34, 45);
Line L2 = Line(10, 10, 100, 100);
L1.drawLn(); // draws from 12,23 to 34,45 L2.drawLn(); // draws from 10,10 to 100,100 } |
Additional Methods for class Line
Now that we have the Line class, we can add functionality to it by adding useful methods. Suppose we will need to know the length of a line. We can easily write a method to return the length of the line.
Add a method prototype inside the class definition:
double length();
Then write the method (outside the class definition):
double Line::length()
{
return sqrt((x2 x1)*(x2 x1) + (y2 y1)*(y2 y1));
}
Then in main(), you could write:
cout << "The length of L is: " << L.length();
Guarding the Data
One important advantage of OOP is that since the data for an object is kept inside that object and is (normally made) invisible (hidden) to code outside the object, we can make sure that the data is always valid and consistent by making checks in the objects code.
For example, suppose we wanted to be sure that all our Lines always were entirely on-screen (assume a 640 x 480 screen). We could write the constructor so that we would initialize the xs and ys to valid values no matter what the passed-in values were:
Line::Line(int left, int top, int right, int bottom)
{
if (left < 0 || left > 639)
x1 = 0;
else
x1 = left;
if (top < 0 || top > 479)
y1 = 0;
else
y1 = top;
// same stuff for right and bottom }
Now with this code its impossible for a Line to be initialized partly or wholly off-screen. And since the xs and ys are by default hidden, nobody else can ever make them invalid.
But. What if main() (or some other function) needs to change the position of a line? If its xs and ys are inaccessible, how can we do that?
Access Methods and Private/Public
First an additional detail about classes. Data and methods can be
Note: the Line class definition earlier omitted one detail: the specification of what parts of the class are public and what parts are private.
To answer the question above: how can we arrange to alter the values of an object's x's and y's: Simple - we make the x's and y's private, but we write some public methods to allow other code access to the xs and ys. And we write the code in those public methods to not allow invalid values.
A correct Line class definition, with a couple of new methods follows:
class Line
{
private:
int x1, y1, x2, y2; // the class variables
public: Line(int, int, int, int); // the "constructor" void drawLn(); // the draw method double length(); void setTop(int); // access methods void setLeft(int); // to set the void setBottom(int); // xs and ys void setRight(int); // of the object };
void Line::setTop(int newTop)
{
if (newTop < 0 || newTop > 479)
y1 = 0;
else
y1 = newTop;
}
The other 3 are similar. Now main() (or any other function with access to a Line object) can reset one or more of the xs and ys but never to an offscreen position:
L1.setTop(333);
L2.setLeft(someNewPos); //might be wrong = offscreen
If you entirely leave out public: and private: the default in C++ is private: So all data and methods are private and thus the class is not much good.
Notice that if we want to change all 4 xs and ys, and we have only the methods written so far, wed have to make 4 setXXX calls:
L1.setLeft(12); L1.setTop(23); L1.setRight(34); L1.setBottom(45);
So suppose we want to write a method that will allow us to specify all 4 xs and ys in one call. That’s easy, too. Notice that rather than recode each condition, we just use the setXXX() methods inside the new method. In fact, we should go back and change the constructor to use these, now that we have them. In this way, the detailed code to set a coordinate is in just one place (and is called from multiple places). If we ever need to change the way that any of these work (say to use the actual screen size rather than a hard-coded value) we can just change it once, in the setXXX() method, rather than having to hunt it down in 3 or more separate places.
So here is setLine():
void Line::setLine(int left, int top, int right, int bottom)
{
setLeft(left);
setTop(top);
setRight(right);
setBottom(bottom);
}
And now we can do this:
L1.setLine(12, 23, 34, 45);
And, by the way, here is the new constructor:
Line::Line(int left, int top, int right, int bottom)
{
setLine(left, top, right, bottom);
}
Ok. Now suppose we want to set just the ys, or just the xs, or just the left and the bottom, or everything except the right.
We could write more functions to do each and every possible combination, but that seems like a bother.
Why cant we just call setLine() using the current values of the ones we dont want to change:
L1.setLine(22, theCurrentY1, 44, theCurrentY2);
Well - how can the calling code here know theCurrent whatever? All the xs and ys are private == hidden inside L1. We cant change them or even see their values from here. This is code in main() or some function, not code in one of the Line objects methods.
The answer is easy write some more public methods to get the values you want:
int Line::getTop()
{
return y1;
}
int Line::getLeft()
{
return x1;
}
You get the idea. Now, to use them:
L1.setLine(22, L1.getTop(), 44, L1.getBottom());
In other classes, there may be many more data members which you usually will make private. If there might be a reason for code outside the object to know the values of the internal data members, you make a simple getXXX method for each such data member.
Most classes, then, have a set of getXXX and setXXX methods.
The getXXX methods usually have no arguments and usually simply return the value of a particular data member. Sometimes they might return a value in a different form.
Suppose you have a Time class and you represent the time as a data member containing the number of seconds since midnight. You could write a getTime() method to return this int number of seconds - but that would not mean much to most people. You might instead (or in addtion) write a getTimeInHrMinSec() to return a string that looks like "10:24:30 am".
The setXXX methods usually take one or more arguments and do some kind of validity or consistency checking of their argument(s) before assigning the arguments value to a data member. They may or may not print an error message, return a success/error code, or "throw an exception" (another error handling system in C++).
Method Overloading
Sometimes its convenient to have different versions of a method that do the same thing. That is, several methods with the same name, but which take different arguments.
Overloading constructors is more common than overloading other methods.
As an example, recall the constructor for the Line class. It takes 4 integer arguments.
But lets suppose you have made the following declaration:
struct TPoint
{
int x, y;
};
and in your program you have created and initialized a couple of these:
Tpoint Pt1 = {20, 30},
Pt2 = {40, 50};
Now you could do the following:
Line L3 = Line(Pt1.x, Pt1.y, Pt2.x, Pt2.y);
But it would be nice if you could just do this:
Line L3 = Line(Pt1, Pt2);
And guess what? You can.
Just write a new constructor:
Line::Line(Tpoint p1, Tpoint p2)
{
if (p1.x < 0 || p1.x > 679)
x1 = 0;
else
x1 = p1.x;
//etc. for the rest. }
Or:
Line::Line(Tpoint p1, Tpoint p2)
{
setLine(p1.x, p1.y, p2.x, p2.y);
}
(Of course, anytime you write any new method, you must write the prototype of the method in the class definition.)
So in a program that uses Lines, you are now free to create Lines using either of the constructors:
Line L4 = Line(xx, yy, 0, max(pp, qq));
Line L5 = Line(Pt1, Pt2);
The rule for creating different methods with the same name is that the number, order, or type of arguments must be different in each case (so the compiler can tell which one to call). The return value has nothing to do with it. So you could have the following:
void dog(int, int)
int dog(int, double);
void dog(double, int);
etc.
Here's another example. Suppose you have a private int data member in an object called num, and you want to write a setNum() method. That's easy:
int SomeClass::setNum(int i)
{
if (some guarding/validation condition)
{
num = i;
return 1; // = success
}
else
return 0; // = failure
}
Now you also realize that sometimes the calling program will have the numeric value they want to use stored in a string. Maybe it's a command line argument stored in argv[x]. Or maybe it had been read in via a getline() call (see Notes on "Input and Output"). In any case, it's a string. So, for the convenience of the calling program, you can write an overloaded version of setNum() that takes a string as an argument:
int ClassType::setNum(char i[])
{
// local vars...
// code to do this:
// check i - make sure all chars are in the range '0' to '9'
// if not
// return 0 (= fail) so caller knows this failed
// else
// convert it to an integer (use sscanf() or atoi())
// and store it into num
// and return 1 (= success)
}
The informal rule is that functions with the same name should do the same thing. You can sometimes see the following:
int length();
void length(int);
The first was used to return (i.e. get) the length of something:
int len = obj.length();
The second was used to set the length of that thing.
obj.length(10);
Although they both have something to do with length, they do very different things, so this is not a good use of this feature.
You will learn much more about object oriented programming in future courses (241 and 440) if you take them. You'll also learn more about C++ - pointers-to-pointers, dynamic memory allocation, and so on. There's a lot more to know, but you know a lot already to build on.