C++ For Dummies (2014)
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 and a toaster can't make nachos.
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 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.)
Figure 21-1: Independent classes Checking and Savings.
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.
Figure 21-2: Savings implemented as a subclass of Checking.
Although letting Savings inherit from Checking is laborsaving, it’s not completely satisfying. The main problem is that, like the weight listed on my driver’s license, it 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 minimumBalance 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, as shown in Figure 21-3.
Figure 21-3: Basing Checking and Savings on a common Account class.
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 affecting 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.
Implementing Abstract Classes
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.
“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.
Describing the abstract class concept
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 — unfortunately, C++ uses this term to mean polymorphic. See Chapter 20).
The syntax for declaring a function pure virtual is demonstrated in the following class Account:
// Account - this class is an abstract class
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;
// keep accounts in a linked list so there's no limit
// to the number of accounts
static int count; // number of accounts
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. Presumably, at least at one time, the entry for a pure virtual function was 0. In any case, it's the syntax we're stuck with now.
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 technical term is to instantiate. We say that the Account class cannot be instantiated with an object or a given object instantiates the Savings class.
Making an honest class out of an abstract class
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.
Passing abstract classes
Because you can’t instantiate 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
// same here
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 non-abstract 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 non-abstract 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 online material at www.dummies.com/extras/cplusplus 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.