In the previous lecture we discussed the doublelist
module and the DoubleNode
class from Lab01, and we pointed out how it did not completely realize our ideal of separating interface from implementation. The problem was this: even though the user of our module had functions with clear interfaces (prototypes) for interacting with our module, he still needed to understand the implementation of the module. Specifically, he had to develop his own function/method for getting the data out of the module, which also required that he know the implementation details for how the data was stored in the DoubleNode, and be provided access to them.
Let's illustrate this with a slightly different example. Suppose we write a module to deal with times of day - i.e. clocks. The module consists of ToD.h and ToD.cpp (which are horribly undocumented, but that's to save space in these notes!). The interface is the important part if we want to use this, so here's the .h file:
/**************************************************
* A simple module for keeping track of the time of day
* using 12 or 24 hour clocks. The provided comments &
* documentation is inadequate, but it serves a purpose
* to keep the space down so it fits easily in the notes.
**************************************************/
#ifndef _TOD_
#define _TOD_
#include <fstream>
#include <string>
using namespace std;
// Class for storing times of day
class ToD{
public:
int h, m;
bool PM;
};
// Func's: setting, printing, and adding time
bool set12(ToD &T, int lh, int bh, string ampm);
bool set24(ToD &T, int lh, int bh);
void print12(ToD T, ostream&);
void print24(ToD T, ostream&);
void addmints(ToD &T, int m);
void addhours(ToD &T, int h);
#endif
In implementing this module, certain assumptions are made about an object of class ToD: namely that the member h
is always in the range 1..12, and the member m
is always in the range 0..59. The code will break if that isn't the case. The user of this module has to know enough about the implementation not to break this scheme - meaning he either knows not to mess with the data members in a ToD
object or he knows enough to mess with the data members and ensure that the assumptions are always true.
main1.cpp | main2.cpp | main3.cpp |
|
|
|
Output:
|
Output:
|
Output:
|
In main1.cpp
the user of the module has used it properly. In main2.cpp
the user of the class has decided to initialize the ToD
for himself, but he's bungled it - as the output shows. Still, if you looked at the program you'd assume that the bug was in the ToD
module, wouldn't you? The problem is that the user of the module has access to the implementation, and as long as that's possible it's also possible that he may mess up, however inadvertantly. In main3.cpp
the user forgot to initialize the time at all. His fault, you might say, and that's true. However, once again from the outside it looks like the ToD
module is the one spewing out nonsense.
To make sure that the module always functions properly, we need to
main2.cpp
), andToD
so that even if the user forgets to initialize the ToD
to the right value for his application, at least it gets initialized to a value that respects the assumptions about theh
and m
data members so that we avoid the implementation printing out nonsense (as in main3.cpp
).In this lecture we'll learn how (1) can be accomplished in C++, and in the next lecture we'll learn how (2) can be accomplished in C++.
You've probably wondered about the word "public" that appears in all of our class definitions. The public tag states that all the class data members that follow are accessibly by anybody or anyone (via the .
or ->
operators). However, you can declare data members to be private instead, in which case the data cannot be accessed this way. Here's an example:
PPEx1.cpp
class Example {
public:
double x, y;
private:
char code;
};
int main() {
Example E;
E.x = 1.5;
E.y = -0.5;
E.code = 'X';
return 0;
}
Compliation
You can see that the compilation failed, and a bit of investigation would tell you that the line E.code = 'X';
is the culprit. You see, by declaring char code
to be private, it cannot be accessed within main
. So, we can lock out any user of our ToD
class from above in the same way, we simply declare the class as:
// Class for storing times of day
class ToD {
private:
int h, m;
bool PM;
};
This will lock out any user of the ToD
class from manipulating any of the data members of the class ... unfortunately it locks us out as well, meaning that the functions we provided for manipulating the class - like void addhours(ToD &T, int h);
- can't access any of those data members of class ToD
either. Clearly there must be some way that someone can access those data members. Otherwise the class is useless.
One of the major tenants of object oriented programming is that data and the functions that operate on that data should be packaged up together. Just as different data is packed together as members of a single class, funtions ought to be allowed as members as well. Why bring it up now? Well, these member functions have access to private data! Here I'll declare a member function f()
with return type void
of class ToD
. Notice that f
is listed under the public members of ToD
. Access specifiers affect member functions just like member data. If f
were declared private, nobody could call it!
// Class for storing times of day
class ToD {
private:
int h, m;
bool PM;
public:
void f(); // our first member function!
};
Now, the secret to understanding member functions is to remember that just as each object of type ToD
has its own copies of h
, m
and PM
, each object of type ToD
has its own copy of the function f
. So if T
is an object of type ToD
, just as we say T.h
to access the data member h
belonging to T
, we say T.f()
to call the member function f
belonging to T
. Just as T.h
and S.h
, where T
and S
are of type ToD
refer to differentint
objects, T.f()
and S.f()
are different function calls! As we define member functions, you'll see just how they differ.
To define a member function you need to know that the name of, for example, our member function f
from above is not really f
. It's actually ToD::f
. The ToD::
specifies that the function belongs to the class ToD
. Inside of the class definition you may simply say f
because their is no conflict or confusion about the name of the funtion. Outside of the class definition you need the full name, however, which is ToD::f
, because there's nothing to stop another class from having a funtion named f
, so we need a way to know which f
we want. So, in ToD.cpp
we might place a definition of our member function like this:
void ToD::f() {
// Do something!
}
Now, because each object of type ToD
has its own member function f
(just as it has its own data members h
, m
and PM
), you may use a name like h
unambiguously - you mean the data member h
belonging to the same object that the function f
belongs to. For example, suppose we define f
as follows:
void ToD::f() {
cout << "Hour is " << h << endl;
}
Suppose that S
and T
are of type ToD
, and S.h
is 3 and T.h
is 10. Then T.f()
will print the value of h
in object T
(i.e. 10), while S.f()
will print the value of h
in S
(i.e. 3).
Before we go any further, we should talk about the concept of data access. There are two types of access to data: read access and write access. Granting someone write access is a lot riskier than granting them read access. The user may get his code in trouble with read access, but he can't usually mess up your code if you haven't granted him write access. For our ToD
class, read access would mean public member functions returning the h
-value, m
-value and PM
-value. There's a way to do this inline
, which we'll discuss next time, but for now we'll define the member functions outside of the class.
Let's also create a new public member function called set12
which will modify (write to) h
and m
based on values passed to the function. Because the function will belong to the ToD
object with which it's called (remember, T.set12(...)
is the way it's called), there will be no reason to pass set12
a ToD
object to modify. Which ToD
object should it modify? The one to which it belongs!
|
|
|
Notice that the read access function for AM vs. PM information return the string "AM" or "PM", rather than the value of the boolean data member. This way information is read from the object in the same way as it's written to the object. Returning the boolean data member value instead would start to involve the user in the implementation again.
You'll also notice the choice of the words get and set. These words are common across a number of languages and are an easy and straightforward way of telling a user the purpose of a function. To be explicit, get tells a user that the function will only read data, while set tells the user that the function will write data. You should make use of get and set in your own code or find an equally clear way to express purpose!
To impress upon you what this idea of member functions belonging to an object, here's an illustration: