In This Chapter
Factoring common properties into a base class
Using abstract classes to hold factored information
Declaring abstract classes
Inheriting from an abstract class
Dividing a program into multiple modules using a project file
The concept of inheritance allows one class to inherit the properties of a base class. Inheritance has a number of purposes, including paying for my son's college. The main benefit of inheritance is the ability to point out the relationship between classes. This is the so-called IS_A relationship — a MicrowaveOven IS_A Oven
and stuff like that.
Factoring is great stuff if you make the correct correlations. For example, the microwave versus conventional oven relationship seems natural. Claim that microwave is a special kind of toaster, and you're headed for trouble. True, they both make things hot, they both use electricity, and they're both found in the kitchen, but the similarity ends there — a microwave can't make toast.
Identifying the classes inherent in a problem and drawing the correct relationships among these classes is a process known as factoring. (The word is related to the arithmetic that you were forced to do in grade school: factoring out the least common denominators, for example, 12 is equal to 2 times 2 times 3.)
This section describes how you can use inheritance to simplify your programs using a bank account example. Suppose that you were asked to a write a simple bank program that implemented the concept of a savings account and a checking account.
I can talk until I'm blue in the face about these classes; however, object-oriented programmers have come up with a concise way to describe the salient points of a class in a drawing. The Checking
and Savings
classes are shown in Figure 21-1. (This is only one of several ways to graphically express the same thing.)
To read this figure and the other figures, remember the following:
The big box is the class, with the class name at the top.
The names in boxes are member functions.
The names not in boxes are data members.
The names that extend partway out of the boxes are publicly accessible members; that is, these members can be accessed by functions that are not part of the class or any of its descendents. Those members that are completely within the box are not accessible from outside the class.
A thick arrow (see Figure 21-2) represents the IS_A relationship.
A thin arrow represents the HAS_A relationship.
A Car
IS_A Vehicle
, but a Car
HAS_A Motor
.
You can see in Figure 21-1 that the Checking
and Savings
classes have a lot in common. For example, both classes have a withdrawal()
and deposit()
member function. Because the two classes aren't identical, however, they must remain as separate classes. (In a real-life bank application, the two classes would be a good deal more different than in this example.) Still, there should be a way to avoid this repetition.
You could have one of these classes inherit from the other. Savings
has more members than Checking
, so you could let Savings
inherit from Checking
. This arrangement is shown in Figure 21-2. The Savings
class inherits all the members. The class is completed with the addition of the data member noWithdrawals
and by overriding the function withdrawal()
. You have to override withdrawal()
because the rules for withdrawing money from a savings account are different from those for withdrawing money from a checking account.
Although letting Savings
inherit from Checking
is laborsaving, it's not completely satisfying. The main problem is that it, like the weight listed on my driver's license, misrepresents the truth. This inheritance relationship implies that a savings account is a special type of checking account, which it is not.
"So what?" you say. "Inheriting works, and it saves effort." True, but my reservations are more than stylistic trivialities — my reservations are at some of the best restaurants in town (at least that's what all the truckers say). Such misrepresentations are confusing to the programmer, both today's and tomorrow's. Someday, a programmer unfamiliar with our programming tricks will have to read and understand what our code does. Misleading representations are difficult to reconcile and understand.
In addition, such misrepresentations can lead to problems down the road. Suppose, for example, that the bank changes its policies with respect to checking accounts. Say it decides to charge a service fee on checking accounts only if the minimum balance dips below a given value during the month.
A change like this can be easily handled with minimal changes to the class Checking
. You'll have to add a new data member to the class Checking
to keep track of the minimum balance during the month. Let's go out on a limb and call it minimumBalance
.
But now you have a problem. Because Savings
inherits from Checking, Savings
gets this new data member as well. It has no use for this member because the minimum balance does not affect savings accounts, so it just sits there. Remember that every checking account object has this extra minimum Balance
member. One extra data member may not be a big deal, but it adds further confusion.
Changes like this accumulate. Today it's an extra data member — tomorrow it's a changed member function. Eventually, the savings account class is carrying a lot of extra baggage that is applicable only to checking accounts.
Now the bank comes back and decides to change some savings account policy. This requires you to modify some function in Checking
. Changes like this in the base class automatically propagate down to the subclass unless the function is already overridden in the subclass Savings
. For example, suppose that the bank decides to give away toasters for every deposit into the checking account. (Hey — it could happen!) Without the bank (or its programmers) knowing it, deposits to checking accounts would automatically result in toaster donations. Unless you're very careful, changes to Checking
may unexpectedly appear in Savings
.
How can you avoid these problems? Claiming that Checking
is a special case of Savings
changes but doesn't solve our problem. What you need is a third class (call it Account
, just for grins) that embodies the things that are common between Checking
and Savings
. This relationship is shown in Figure 21-3.
How does building a new account solve the problems? First, creating a new Account
class is a more accurate description of the real world (whatever that is). In our concept of things (or at least in mine), there really is something known as an account. Savings accounts and checking accounts are special cases of this more fundamental concept.
In addition, the class Savings
is insulated from changes to the class Checking
(and vice versa). If the bank institutes a fundamental change to all accounts, you can modify Account
, and all subclasses will automatically inherit the change. But if the bank changes its policy only for checking accounts, you can modify just the Checking
account class without modifying Savings
.
This process of culling common properties from similar classes is the essence of class factoring.
Factoring is legitimate only if the inheritance relationship corresponds to reality. Factoring together a class Mouse
and Joystick
because they're both hardware pointing devices is legitimate. Factoring together a class Mouse
and Display
because they both make low-level operating system calls is not.
As intellectually satisfying as factoring is, it introduces a problem of its own. Return one more time to the bank account classes, specifically the common base class Account
. Think for a minute about how you might go about defining the different member functions defined in Account
.
Most Account
member functions are no problem because both account types implement them in the same way. Implementing those common functions with Account
::withdrawal()
is different, however. The rules for withdrawing from a savings account are different than those for withdrawing from a checking account. You'll have to implement Savings::withdrawal()
differently than you do Checking::withdrawal()
. But how are you supposed to implement Account::withdrawal()
?
Let's ask the bank manager for help. I imagine the conversation going something like the following:
"What are the rules for making a withdrawal from an account?" you ask expectantly.
"What type of account? Savings or checking?" comes the reply.
"From an account," you say. "Just an account."
Blank look. (One might say a "blank bank look" ... then again, maybe not.)
The problem is that the question doesn't make sense. There's no such thing as "just an account." All accounts (in this example) are either checking accounts or savings accounts. The concept of an account is an abstract one that factors out properties common to the two concrete classes. It is incomplete because it lacks the critical property withdrawal()
. (After you get further into the details, you may find other properties that a simple account lacks.)
An abstract class is one that exists only in subclasses. A concrete class is a class that is not abstract. Hardly an abstract concept.
C++ supports a concept known as an abstract class to describe an incomplete concept such as an account.
An abstract class is a class with one or more pure virtual functions. Oh, great! That helps a lot.
Okay, a pure virtual function is a virtual member function that is marked as having no implementation. Most likely it has no implementation because no implementation is possible with the information provided in the class, including any base classes. A conventional, run-of-the-mill non-pure virtual function is known as a concrete function (note that a concrete function may be virtual).
The syntax for declaring a function pure virtual is demonstrated in the following class Account
:
// Account - this class is an abstract class class Account { public: Account(unsigned accNo, double initialBalance = 0.0); // access functions unsigned int accountNo( ); double acntBalance( ); static int noAccounts( ); // transaction functions void deposit(double amount); // the following is a pure virtual function virtual void withdrawal(double amount) = 0; protected: // keep accounts in a linked list so there's no limit // to the number of accounts
static int count; // number of accounts unsigned accountNumber; double balance; };
The = 0
after the declaration of withdrawal()
indicates that the programmer does not intend to define this function. The declaration is a placeholder for the subclasses. The subclasses of Account
are expected to override this function with a concrete function. The programmer must provide an implementation for each member function not declared pure virtual.
I think this notation is silly, and I don't like it any more than you do. But it's here to stay, so you just have to learn to live with it. There is a reason, if not exactly a justification, for this notation. Every virtual function must have an entry in a special table. This entry contains the address of the function. The entry for a pure virtual function is 0. Some other languages define an abstract keyword — no, I mean a keyword abstract.
An abstract class cannot be instanced with an object; that is, you can't make an object out of an abstract class. For example, the following declaration is not legal:
void fn( ) { // declare an account with 100 dollars Account acnt(1234, 100.00);// this is not legal acnt.withdrawal(50); // what would you expect // this call to do? }
If the declaration were allowed, the resulting object would be incomplete, lacking in some capability. For example, what should the preceding call do? Remember, there is no Account::withdrawal()
.
Abstract classes serve as base classes for other classes. An Account
contains all the properties associated with a generic bank account. You can create other types of bank accounts by inheriting from Account
.
The subclass of an abstract class remains abstract until all pure virtual functions have been overridden. The class Savings
is not abstract because it overrides the pure virtual function withdrawal()
with a perfectly good definition. The class Savings
knows how to perform withdrawal()
when called on to do so. So does the class Checking
even if the answer is different. Neither class is virtual because the function withdrawal()
overrides the pure virtual function in the base class.
Because you can't instance an abstract class, it may sound odd that it's possible to declare a pointer or a reference to an abstract class. With polymorphism, however, this isn't as crazy as it sounds. Consider the following code snippet:
void fn(Account *pAccount); // this is legal void otherFn( ) { Savings s; Checking c; // this is legitimate because Savings IS_A Account fn(&s); // same here fn(&c); }
Here, pAccount
is declared as a pointer to an Account
. However, it's understood that when the function is called, it will be passed the address of some nonabstract subclass object such as Savings
or Checking
.
All objects received by fn()
will be of either class Savings
or class Checking
(or some future equally nonabstract subclass of Account
). The function is assured that you will never pass an actual object of class Account
because you could never create one to pass in the first place.
The enclosed CD-ROM includes a set of programs Budget1 through Budget5. Each program solves essentially the same problem. Each program allows the user to create and collect the balance of a series of checking and savings accounts. However, each program in the sequence is a bit more object-oriented than its predecessors. Budget1 is a completely functional implementation with no concept of classes. Budget2 implements separate Savings
and Checking
classes. The Budget3 program factors the similarities in these two classes into a common, abstract Account
class using the techniques presented in this chapter. Budget4 and Budget5 go on to use features presented in the following chapters.
3.140.196.244