- •Table of Contents
- •Introduction
- •What Is C++?
- •Conventions Used in This Book
- •How This Book Is Organized
- •Part I: Introduction to C++ Programming
- •Part III: Introduction to Classes
- •Part IV: Inheritance
- •Part V: Optional Features
- •Part VI: The Part of Tens
- •Icons Used in This Book
- •Where to Go from Here
- •Grasping C++ Concepts
- •How do I program?
- •Installing Dev-C++
- •Setting the options
- •Creating Your First C++ Program
- •Entering the C++ code
- •Building your program
- •Executing Your Program
- •Dev-C++ is not Windows
- •Dev-C++ help
- •Reviewing the Annotated Program
- •Examining the framework for all C++ programs
- •Clarifying source code with comments
- •Basing programs on C++ statements
- •Writing declarations
- •Generating output
- •Calculating Expressions
- •Storing the results of expression
- •Declaring Variables
- •Declaring Different Types of Variables
- •Reviewing the limitations of integers in C++
- •Solving the truncation problem
- •Looking at the limits of floating-point numbers
- •Declaring Variable Types
- •Types of constants
- •Special characters
- •Are These Calculations Really Logical?
- •Mixed Mode Expressions
- •Performing Simple Binary Arithmetic
- •Decomposing Expressions
- •Determining the Order of Operations
- •Performing Unary Operations
- •Using Assignment Operators
- •Why Mess with Logical Operations?
- •Using the Simple Logical Operators
- •Storing logical values
- •Using logical int variables
- •Be careful performing logical operations on floating-point variables
- •Expressing Binary Numbers
- •The decimal number system
- •Other number systems
- •The binary number system
- •Performing Bitwise Logical Operations
- •The single bit operators
- •Using the bitwise operators
- •A simple test
- •Do something logical with logical calculations
- •Controlling Program Flow with the Branch Commands
- •Executing Loops in a Program
- •Looping while a condition is true
- •Using the for loop
- •Avoiding the dreaded infinite loop
- •Applying special loop controls
- •Nesting Control Commands
- •Switching to a Different Subject?
- •Writing and Using a Function
- •Divide and conquer
- •Understanding the Details of Functions
- •Understanding simple functions
- •Understanding functions with arguments
- •Overloading Function Names
- •Defining Function Prototypes
- •Variable Storage Types
- •Including Include Files
- •Considering the Need for Arrays
- •Using an array
- •Initializing an array
- •Accessing too far into an array
- •Using arrays
- •Defining and using arrays of arrays
- •Using Arrays of Characters
- •Creating an array of characters
- •Creating a string of characters
- •Manipulating Strings with Character
- •String-ing Along Variables
- •Variable Size
- •Address Operators
- •Using Pointer Variables
- •Comparing pointers and houses
- •Using different types of pointers
- •Passing Pointers to Functions
- •Passing by value
- •Passing pointer values
- •Passing by reference
- •Limiting scope
- •Examining the scope problem
- •Providing a solution using the heap
- •Defining Operations on Pointer Variables
- •Re-examining arrays in light of pointer variables
- •Applying operators to the address of an array
- •Expanding pointer operations to a string
- •Justifying pointer-based string manipulation
- •Applying operators to pointer types other than char
- •Contrasting a pointer with an array
- •Declaring and Using Arrays of Pointers
- •Utilizing arrays of character strings
- •Identifying Types of Errors
- •Choosing the WRITE Technique for the Problem
- •Catching bug #1
- •Catching bug #2
- •Calling for the Debugger
- •Defining the debugger
- •Finding commonalities among us
- •Running a test program
- •Single-stepping through a program
- •Abstracting Microwave Ovens
- •Preparing functional nachos
- •Preparing object-oriented nachos
- •Classifying Microwave Ovens
- •Why Classify?
- •Introducing the Class
- •The Format of a Class
- •Accessing the Members of a Class
- •Activating Our Objects
- •Simulating real-world objects
- •Why bother with member functions?
- •Adding a Member Function
- •Creating a member function
- •Naming class members
- •Calling a Member Function
- •Accessing a member function
- •Accessing other members from a member function
- •Defining a Member Function in the Class
- •Keeping a Member Function After Class
- •Overloading Member Functions
- •Defining Arrays of and Pointers to Simple Things
- •Declaring Arrays of Objects
- •Declaring Pointers to Objects
- •Dereferencing an object pointer
- •Pointing toward arrow pointers
- •Passing Objects to Functions
- •Calling a function with an object value
- •Calling a function with an object pointer
- •Calling a function by using the reference operator
- •Returning to the Heap
- •Comparing Pointers to References
- •Linking Up with Linked Lists
- •Performing other operations on a linked list
- •Hooking up with a LinkedListData program
- •A Ray of Hope: A List of Containers Linked to the C++ Library
- •Protecting Members
- •Why you need protected members
- •Discovering how protected members work
- •Protecting the internal state of the class
- •Using a class with a limited interface
- •Creating Objects
- •Using Constructors
- •Why you need constructors
- •Making constructors work
- •Dissecting a Destructor
- •Why you need the destructor
- •Working with destructors
- •Outfitting Constructors with Arguments
- •Justifying constructors
- •Using a constructor
- •Defaulting Default Constructors
- •Constructing Class Members
- •Constructing a complex data member
- •Constructing a constant data member
- •Constructing the Order of Construction
- •Local objects construct in order
- •Static objects construct only once
- •Global objects construct in no particular order
- •Members construct in the order in which they are declared
- •Destructors destruct in the reverse order of the constructors
- •Copying an Object
- •Why you need the copy constructor
- •Using the copy constructor
- •The Automatic Copy Constructor
- •Creating Shallow Copies versus Deep Copies
- •Avoiding temporaries, permanently
- •Defining a Static Member
- •Why you need static members
- •Using static members
- •Referencing static data members
- •Uses for static data members
- •Declaring Static Member Functions
- •What Is This About, Anyway?
- •Do I Need My Inheritance?
- •How Does a Class Inherit?
- •Using a subclass
- •Constructing a subclass
- •Destructing a subclass
- •Having a HAS_A Relationship
- •Why You Need Polymorphism
- •How Polymorphism Works
- •When Is a Virtual Function Not?
- •Considering Virtual Considerations
- •Factoring
- •Implementing Abstract Classes
- •Describing the abstract class concept
- •Making an honest class out of an abstract class
- •Passing abstract classes
- •Factoring C++ Source Code
- •Defining a namespace
- •Implementing Student
- •Implementing an application
- •Project file
- •Creating a project file under Dev-C++
- •Comparing Operators with Functions
- •Inserting a New Operator
- •Overloading the Assignment Operator
- •Protecting the Escape Hatch
- •How Stream I/O Works
- •The fstream Subclasses
- •Reading Directly from a Stream
- •Using the strstream Subclasses
- •Manipulating Manipulators
- •Justifying a New Error Mechanism?
- •Examining the Exception Mechanism
- •What Kinds of Things Can I Throw?
- •Adding Virtual Inheritance
- •Voicing a Contrary Opinion
- •Generalizing a Function into a Template
- •Template Classes
- •Do I Really Need Template Classes?
- •Tips for Using Templates
- •The string Container
- •The list Containers
- •Iterators
- •Using Maps
- •Enabling All Warnings and Error Messages
- •Insisting on Clean Compiles
- •Limiting the Visibility
- •Avoid Overloading Operators
- •Heap Handling
- •Using Exceptions to Handle Errors
- •Avoiding Multiple Inheritance
- •Customize Editor Settings to Your Taste
- •Highlight Matching Braces/Parentheses
- •Enable Exception Handling
- •Include Debugging Information (Sometimes)
- •Create a Project File
- •Customize the Help Menu
- •Reset Breakpoints after Editing the File
- •Avoid Illegal Filenames
- •Include #include Files in Your Project
- •Executing the Profiler
- •System Requirements
- •Using the CD with Microsoft Windows
- •Using the CD with Linux
- •Development tools
- •Program source code
- •Index
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.