Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ For Dummies (2004) [eng].pdf
Скачиваний:
84
Добавлен:
16.08.2013
Размер:
8.09 Mб
Скачать

276 Part IV: Inheritance

You can also review the program PolymorphicNachos on the enclosed

CD-ROM for a further example of polymorphism.

When Is a Virtual Function Not?

Just because you think that a particular function call is bound late doesn’t mean that it is. If not declared with the same arguments in the subclasses, the member functions are not overridden polymorphically, whether or not they are declared virtual.

One exception to the identical declaration rule is that if the member function in the base class returns a pointer or reference to a base class object, an overridden member function in a subclass may return a pointer or reference to an object of the subclass. In other words, the function makeACopy() is polymorphic even though the return type of the two functions have a differ­ ent return type:

class Base

{

public:

// return a copy of the current object Base* makeACopy()

{

// ...do whatever it takes to make a copy

}

};

class SubClass : public Base

{

public:

// return a copy of the current object SubClass* makeACopy()

{

// ...do whatever it takes to make a copy

};

};

void fn(Base& bc)

{

BaseClass* pCopy = bc.makeACopy();

// proceed on...

}

In practice, this is quite natural. A makeACopy() function should return an object of type SubClass, even though it might override

BaseClass::makeACopy().

Chapter 21: Examining Virtual Member Functions: Are They for Real? 277

Considering Virtual Considerations

You need to keep in mind a few things when using virtual functions.

First, static member functions cannot be declared virtual. Because static member functions are not called with an object, there is no runtime object upon which to base a binding decision.

Second, specifying the class name in the call forces a call to bind early, whether or not the function is virtual. For example, the following call is to Base::fn() because that’s what the programmer indicated, even if fn() is declared virtual:

void test(Base& b)

{

b.Base::fn();

// this call is not bound late

}

 

Finally, constructors cannot be virtual because there is no (completed) object to use to determine the type. At the time the constructor is called, the memory that the object occupies is just an amorphous mass. It’s only after the constructor has finished that the object is a member of the class in good standing.

By comparison, the destructor should almost always be declared virtual. If not, you run the risk of improperly destructing the object, as in the following circumstance:

class Base

{

public:

~Base();

};

class SubClass : public Base

{

public:

~SubClass();

};

void finishWithObject(Base* pHeapObject)

{

//...work with object...

//now return it to the heap

delete pHeapObject; // this calls ~Base() no matter } // the runtime type of

// pHeapObject

278 Part IV: Inheritance

If the pointer passed to finishWithObject() really points to a SubClass, the SubClass destructor is not invoked properly — because the destructor has been not been declared virtual, it’s always bound early. Declaring the destructor virtual solves the problem.

So when would you not want to declare the destructor virtual? There’s only one case. Virtual functions introduce a “little” overhead. Let me be more spe­ cific. When the programmer defines the first virtual function in a class, C++ adds an additional, hidden pointer — not one pointer per virtual function, just one pointer if the class has any virtual functions. A class that has no vir­ tual functions (and does not inherit any virtual functions from base classes) does not have this pointer.

Now, one pointer doesn’t sound like much, and it isn’t unless the following two conditions are true:

The class doesn’t have many data members (so that one pointer repre­ sents a lot compared to what’s there already).

You intend to create a lot of objects of this class (otherwise, the over­ head doesn’t make any difference).

If these two conditions are met and your class doesn’t already have virtual member functions, you may not want to declare the destructor virtual.

Except for this one case, always declare destructors to be virtual, even if a class is not subclassed (yet) — you never know when someone will come along and use your class as the base class for her own. If you don’t declare the destructor virtual, document it!

Chapter 22

Factoring Classes

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. It can save programming time by avoiding needless code repetition. Inheritance allows the program to reuse existing classes in new applications by overriding functions.

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 rela­ tionships 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.)

Factoring

This section describes how you can use inheritance to simplify your programs using a simple bank account example.

280 Part IV: Inheritance

Suppose that you were asked to a write a simple bank program that imple­ mented the concept of a savings account and a checking account.

Bonus Chapter 1 on the enclosed CD-ROM features the BUDGET programs, which implement just such a simple bank application.

I can talk until I’m blue in the face about these classes; however, objectoriented 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 22-1. (This is only one of several ways to graphically express the same thing.)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Checking

 

 

 

Savings

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

withdrawal( )

 

 

 

withdrawal( )

 

 

 

 

 

 

deposit( )

 

pFirst

 

deposit( )

 

pFirst

 

Figure 22-1:

 

 

 

 

accountNo( )

 

 

accountNo( )

 

 

 

 

pNext

 

 

pNext

 

Independent

 

first( )

 

count

 

first( )

 

count

 

classes

 

 

 

 

 

 

 

 

 

accountNumber

 

 

 

 

accountNumber

 

 

next( )

 

 

next( )

 

Checking

 

 

balance

 

 

balance

 

and

 

noAccounts( )

 

 

noAccounts( )

 

noWithdrawals

 

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 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 22-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

Chapter 22: Factoring Classes 281

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 22-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 sav­ ings account are different from those for withdrawing money from a checking account. (These rules don’t apply to me because I don’t have any money to withdraw anyway.)

 

 

Checking

 

withdrawal( )

 

 

deposit( )

pFirst

 

accountNo( )

 

pNext

 

 

 

first( )

count

 

next( )

accountNumber

 

balance

 

 

 

noAccounts( )

Figure 22-2:

 

 

Savings

 

Savings

imple­

 

 

 

mented as a

withdrawal( )

noWithdrawals

subclass of

 

 

checking.

 

 

Although letting Savings inherit from Checking is laborsaving, it’s not com­ pletely 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 reser­ vations 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 misrep­ resentations are confusing to the programmer, both today’s and tomorrow’s.

282 Part IV: Inheritance

Someday, a programmer unfamiliar with our programming tricks will have to read and understand what our code does. Misleading representations are dif­ ficult 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 car­ rying 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, sup­ pose that the bank decides to give away toasters for every deposit into the checking account. (Hey — it could happen!) Without the bank (or its pro­ grammers) 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 22-3.

How does building a new account solve the problems? First, creating a new account 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.

Chapter 22: Factoring Classes 283

 

 

 

Account

 

 

 

withdrawal( )

 

 

 

 

deposit( )

pFirst

 

 

 

accountNo( )

 

 

 

pNext

 

 

 

 

 

 

 

first( )

count

 

 

 

next( )

accountNumber

 

 

balance

 

 

 

 

 

 

 

noAccounts( )

 

 

Figure 22-3:

 

 

 

 

Basing

 

 

 

 

Checking

 

 

 

 

and Savings

Checking

 

Savings

on a

 

 

 

 

 

common

withdrawal( )

minimumBalance

withdrawal( )

noWithdrawals

Account

 

 

 

 

class.

 

 

 

 

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 out common properties from similar classes is called 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.

Factoring can and usually does result in multiple levels of abstraction. For example, a program written for a more developed bank may have a class structure such as that shown in Figure 22-4.

Here you see that another class has been inserted between Checking and Savings and the most general class Account. This class, called Conventional, incorporates features common to conventional accounts. Other account types, such as stock market accounts, are also foreseen.

Such multitiered class structures are common and desirable as long as the relationships they express correspond to reality. Note, however, that no one correct class hierarchy exists for any given set of classes.