Chapter 24
In This Chapter
Protecting members of a class
Asking, “Why do that?”
Declaring friends of the class
My goal with this part of the book, starting with Chapter 21, is to model real-world objects in C++ by using the class structure. In Chapter 22, I introduce the concept of member functions in order to assign active properties to the classes. Returning to the microwave oven example in Chapter 21, assigning active properties allows me to give my Oven class properties like cook() and defrost().
However, that’s only part of the story. I still haven’t put a box around the insides of my classes to ward off meddling. I can’t very well hold someone responsible if the microwave catches on fire so long as the insides are exposed to anyone who wants to mess with them.
This chapter “puts a box” around the classes by declaring certain members off-limits to user functions.
Members of a class can be flagged as inaccessible from outside the class with the keyword protected. This is in direct opposition to the public keyword, which designates those members that are accessible to all functions. The public members of a class form the interface to the class (think of the keypad on the front of the microwave oven) while the protected members form the inner workings (“no user-serviceable parts inside”).
Declaring a member protected allows a class to put a protective box around the class. This makes the class responsible for its own internal state. If something in the class gets screwed up, the author of the class has nowhere to look except herself. It’s not fair, however, to ask the programmer to take responsibility for the state of the class if any ol’ function can reach in and muck with it.
In addition, limiting the interface to a class makes the class easier to learn for programmers that use that interface in their programs. In general, I don’t really care how my microwave works inside as long as I know how to use the controls. In a similar fashion, I don’t generally worry about the inner workings of library classes as long as I understand the arguments to the public member functions.
Finally, limiting the class interface to just some choice public functions reduces the level of coupling between the class and the application code.
Note: Coupling refers to how much knowledge the application has of how the class works internally, and vice versa. A tightly coupled class has intimate knowledge of the surrounding application — and uses that knowledge. A loosely coupled class works only through a simple, generic public interface. A loosely coupled class knows little about its surroundings and hides most of its own internal details as well. Loosely coupled classes are easier to test and debug — and easier to replace when the application changes.
I know what you procedural types out there are saying: “You don’t need some fancy feature to do all that. Just make a rule that says certain members are publicly accessible and others are not.” This is true in theory, and I’ve even been on projects that employed such rules, but in practice it doesn’t work. People start out with good intentions, but as long as the language doesn’t at least discourage direct access to protected members, these good intentions get crushed under the pressure to get the product out the door.
Adding the keyword public: to a class makes subsequent members publicly accessible. Adding the keyword protected: makes subsequent members protected, which means they are accessible only to other members of the same class or functions that are specifically declared friends (more on that later in this chapter). They act as toggles — one overrides the other. You can switch back and forth between protected and public as often as you like.
Take, for example, a class Student that describes the salient features of a college student. This class has the following public member functions:
The remaining members of Student should be declared protected to keep prying expressions out of his business.
The following SimpleStudent program defines such a Student class and includes a simple main() that exercises the functions:
//
// SimpleStudent - this program demonstrates how the
// protected keyword is used to protect
// key internal members
//
#include <cstdio>
#include <cstdlib>
#include <iostream>
using namespace std;
class Student
{
protected:
double dGrade; // the student's GPA
int nSemesterHours;
public:
// init() - initialize the student to a legal state
void init()
{
dGrade = 0.0;
nSemesterHours = 0;
}
// getGrade() - return the current grade
double getGrade()
{
return dGrade;
}
// getHours() - get the class hours towards graduation
int getHours()
{
return nSemesterHours;
}
// addGrade - add a grade to the GPA and total hours
double addGrade(double dNewGrade, int nHours)
{
double dWtdHrs = dGrade * nSemesterHours;
dWtdHrs += dNewGrade * nHours;
nSemesterHours += nHours;
dGrade = dWtdHrs / nSemesterHours;
return dGrade;
}
};
int main(int nNumberofArgs, char* pszArgs[])
{
// create a student and initialize it
Student s;
s.init();
// add the grades for three classes
s.addGrade(3.0, 3); // a B
s.addGrade(4.0, 3); // an A
s.addGrade(2.0, 3); // a C (average should be a B)
// now print the results
cout << "Total # hours = " << s.getHours()
<< ", GPA = " << s.getGrade()
<< endl;
// wait until user is ready before terminating program
// to allow the user to see the program results
cout << "Press Enter to continue..." << endl;
cin.ignore(10, '
'),
cin.get();
return 0;
}
This Student protects its members dGrade and nSemesterHours. Outside functions can’t surreptitiously set their own GPA high by slipping in the following:
void MyFunction(Student* pS)
{
// set my grade to A+
pS->dGrade = 3.9; // generates a compiler error
}
This assignment generates a compiler error.
Any function can read a student’s GPA through the function getGrade(). This is known as an access function. However, although external functions can read a value, they cannot change the value via this access function.
The main() function in this program creates a Student object s. It cannot initialize s to some legal state since the data members are protected. Fortunately, the Student class has provided an init() function for main() to call that initializes the data members to their proper starting state.
After initializing s, main() calls addGrade() to add three different courses and prints out the results using the access member functions. The results appear as follows:
Total # hours = 9, GPA = 3
Press Enter to continue …
So what’s the big deal? “Okay,” you say, “I see the point about not letting other functions set the GPA to some arbitrary value, but is that it?” No. A finer point lies behind this loose coupling. I chose to implement the algorithms for calculating the GPA as simply as I possibly could. With no more than five minutes’ thought, I can imagine at least three different ways I could have chosen to store the grades and semester hours internally, each with their own advantages and disadvantages.
For example, I could save each grade — along with the number of semester hours — in an internal array. This would allow the student to review the grades that are going into his GPA.
The point is that the application programmer shouldn’t care. As long as the member functions getGrade() and getHours() calculate the GPA and total number of semester hours accurately, no application is going to care.
Now suppose the school changes the rules for how to calculate the GPA. Suppose, for example, that it declares certain classes to be Pass/Fail, meaning that you get credit toward graduation but the grade in the class doesn’t go into the GPA calculation. This may require a total rewrite of the Student class. That, in turn, would require modification to any functions that rely upon the way that the information is stored internally — that is, any functions that have access to the protected members. However, functions that limit themselves to the public members are unaffected by the change.
That is the true advantage of loose coupling: tolerance to change.
Occasionally, you need to give a non-member function access to the protected members of a class. You can do this by declaring the function to be a friend — which means you don’t have to expose the protected members to everyone by declaring them public.
It’s like giving your neighbor a key to check on your house during your vacation. Giving non-family members keys to the house is not normally a good idea, but it beats the alternative of leaving the house unlocked.
The friend declaration appears in the class that contains the protected member. The friend declaration consists of the keyword friend followed by a prototype declaration. In the following example, the initialize() function is declared as a non-member. However, initialize() clearly needs access to all the data members of the class, protected or not:
class Student
{
friend void initialize(Student*);
protected:
double dGrade; // the student's GPA
int nSemesterHours;
public:
double grade();
int hours();
double addGrade(double dNewGrade, int nHours);
};
void initialize(Student* pS)
{
pS->dGrade = 0.0;
pS->nSemesterHours = 0;
}
A single function can be declared to be a friend of two different classes at the same time. Although this may seem convenient, it tends to bind the two classes together. However, sometimes the classes are bound together by their very nature, as in the following teacher-student example:
class Student; // forward declaration
class Teacher
{
friend void registration(Teacher*, Student*);
protected:
int noStudents;
Student *pList[128];
public:
void assignGrades();
};
class Student
{
friend void registration(Teacher*, Student*);
protected:
Teacher *pTeacher;
int nSemesterHours;
double dGrade;
};
In this example, the registration() function can reach into both the Student object to set the pTeacher pointer and into the Teacher object to add to the teacher’s list of students.
Without the forward declaration to Student, the declaration within Teacher of Student *pList[100] generates a compiler error because the compiler doesn’t yet know what a Student is. Swap the order of the definitions, and the declaration Teacher *pTeacher within Student generates a compiler error because Teacher has not been defined yet.
The forward declaration solves the problem by telling the compiler to be patient — a definition for this new class is coming very soon.
A member of one class can be declared a friend of another class:
class Student;
class Teacher
{
// ...other members...
public:
void assignGrade(Student*, int nHours, double dGrade);
};
class Student
{
friend void Teacher::assignGrade(Student*,
int, double);
// ...other members...
};
An entire class can be declared a friend of another class. This has the effect of making every member function of the class a friend. For example:
class Student;
class Teacher
{
protected:
int noStudents;
Student* pList[128];
public:
void assignGrade(Student*, int nHours, double dGrade);
};
class Student
{
friend class Teacher;
// ...other members...
};
Now every member of Teacher can access the protected members of Student (but not the other way around). Declaring one class to be a friend of another binds the classes together inseparably.
3.135.204.0