15

Inheritance, Polymorphism, and Object-Oriented Design

images To appreciate the distinction between structured (procedural) programming and object-oriented programming.

images To know the characteristics of an object-oriented language.

images To understand the difference between static and dynamic binding of operations to objects.

To be able to:

images Create a new C++ class from an existing class by using inheritance.

images Create a new C++ class hierarchy with a virtual function.

images Apply the object-oriented design methodology to solve a problem.

images Take an object-oriented design and code it in C++.

In Chapter 12, we introduced the concept of data abstraction—the separation of the logical properties of a data type from the details of how it is implemented. We expanded on this concept by defining the notion of an abstract data type (ADT) and by using the C++ class mechanism to incorporate both data and operations into a single data type. In that chapter and Chapter 13, we saw how an object of a given class maintains its own private data and is manipulated by calling its public member functions.

In this chapter, we examine how classes and objects can be used to guide the entire software development process. Although the design phase precedes the implementation phase in the development of software, we reverse the order of presentation in this chapter. We begin with object-oriented programming, a topic that includes design but deals more with implementation issues. We describe the basic principles, terminology, and programming language features associated with the object-oriented approach. After presenting these concrete elements, we look more closely at the design phase—object-oriented design.

15.1 Object-Oriented Programming

As you are now well aware, functional decomposition is a design methodology in which we decompose a problem into modules, each of which is a self-contained collection of steps that solves part of the overall problem. The process of implementing a functional decomposition is often called structured (or procedural) programming. Some modules are translated directly into C++ statements, whereas others are coded as functions. The resulting program is a collection of interacting functions (see FIGURE 15.1). With functional decomposition, data is considered a passive entity to be acted upon.

Functional decomposition is satisfactory for programming in the small (a concept we discussed in Chapter 4) but often does not “scale up” well for programming in the large. When we are building large software systems, this approach has two important limitations.

images

Figure 15.1 Program Resulting from Functional Decomposition

First, the technique yields an inflexible structure. If the top-level algorithm requires modification, the changes may force many lower-level algorithms to be modified as well. Second, the technique does not lend itself easily to code reuse. By code reuse, we mean the ability to use pieces of code—either as they are or adapted slightly—in other sections of the program or in other programs. It is rare to be able to take a complicated C++ function and reuse it easily in a different context.

A methodology that often works better for creating large software systems is object-oriented design (OOD), which we introduced briefly in Chapter 4. OOD decomposes a problem into objects —self-contained entities composed of data and operations on the data. The process of implementing an object-oriented design is called object-oriented programming (OOP). The end result is a program that is a collection of interacting objects (see FIGURE 15.2). In OOD and OOP, data plays a leading role; the primary contribution of algorithms is to implement the operations on objects. In this chapter, we'll see why OOD tends to result in programs that are more flexible and conducive to code reuse than programs produced by functional decomposition.

Several programming languages have been created specifically to support OOD and OOP: C++, Java, Smalltalk, Simula, CLOS, Objective-C, Eiffel, Actor, Python, and others. These languages, which are called object-oriented programming languages, have facilities that support:

images Data abstraction

images Inheritance

images Dynamic binding

images

Figure 15.2 Program Resulting from Object-Oriented Programming

You have already seen that C++ supports data abstraction through the class mechanism. Some non-OOP languages also have facilities for data abstraction. But only OOP languages support the other two concepts—inheritance and dynamic binding.

15.2 Inheritance

In the world at large, it is often possible to arrange concepts into an inheritance hierarchy— that is, a hierarchy in which each concept inherits the properties of the concept immediately above it in the hierarchy. For example, we might classify different kinds of vehicles according to the inheritance hierarchy shown in FIGURE 15.3. Moving down the hierarchy, each kind of vehicle is more specialized than its parent (and all of its ancestors) and is more general than its child (and all of its descendants). A wheeled vehicle inherits properties common to all vehicles (it holds one or more people and carries them from place to place) but has an additional property that makes it more specialized (it has wheels). A car inherits properties common to all wheeled vehicles but also has other, more specialized properties (four wheels, an engine, a body, and so forth).

The inheritance relationship can be viewed as an is-a relationship. Every two-door car is a car, every car is a wheeled vehicle, and every wheeled vehicle is a vehicle. This notion is distinct from the principle of composition, where one object is contained within another object. Composition is a has-a relationship: The enclosing object has a member that is another object.

To better understand the principles underlying inheritance, let's look at an analogy between the work of an architect and the work of a programmer. The way that an architect handles the complexity of a large building design sheds some light on how we can organize our own programming work. This analogy lets us consider the same concepts, but without the distraction of C++ syntax.

An Analogy

The architect begins by considering the overall requirements for a building: square footage, number of occupants, usage, and so on. The architect faces a basic aspect of any design: The building is composed of floors. In many buildings, the floors all have common characteristics: the same size and shape; the same locations for elevator shafts, stairways, and utility trunks; and so on. The architect begins by designing a basic empty floor with these common elements. Once she installs this plan in the library of her computer-aided design (CAD) program, she can use it as the starting point for designing each floor of the building.

images

Figure 15.3 Inheritance Hierarchy

The architect may further decide that the building has two main types of floors: office floors and mechanical equipment floors. The office floors might be of two types: executive office space and standard office space. Starting from the basic empty floor design, the architect adds components such as lavatories and hallways to make an empty office floor. She can then add offices and conference rooms to the empty space. In this way, each of the four types of floor is derived from the basic empty floor and added to the library (see FIGURE 15.4). Drawing the entire building then becomes simply a matter of creating an instance of one of these four floor plans for each story.

The architect uses the same process to design the components that make up the floors. She might design a basic type of office, and then derive several types of offices from that one design. From a given type of office, such as secretarial, she might further refine the design into subtypes such as general secretarial, secretary/receptionist, and executive secretary.

Creating hierarchies of designs simplifies the architect's job. She begins each hierarchy with the most general form of a building component, such as the basic empty floor, and then derives new designs by adding details to the more general forms. The new designs inherit all of the characteristics of the general form, saving the architect from having to redraw the common pieces. In some cases she replaces existing parts of a design, such as when she substitutes a wider door for a reception area than appears in the basic secretarial office. The replacement part overrides what was originally specified in the more general form.

In addition to the components of individual floors, the architect can specify characteristics that are common to all floors, such as a color scheme. Each floor will then inherit these general properties. Sometimes she hides or deletes portions of the general properties, such as when she customizes the color scheme for a particular floor that has been rented in advance by a company with its own corporate colors.

Inheritance and the Object-Oriented Design Process

Now let's consider how a class hierarchy originates in the object-oriented design process. During the problem-solving phase, we discover that we need a class that is a variation of an existing one. For example, in the case study of Chapter 14, we discovered that the Entry class from Chapter 12 was appropriate, but noted that it lacked a ComparedTo member function. We also decided that the class should have a second parameterized constructor that took a Name object and a TimeOfDay object as parameters.

We solved this problem by copying the code for Entry, and then using that copy to create a new class with the extra responsibilities. The result was two classes named Entry that are unrelated. Later, if we find a bug in the original Entry class, we must remember to check the extended class for the same bug. But wouldn't it be nice if a fix or an improvement to the original could be automatically propagated to any derived classes?

We can achieve this goal by making Entry a base class or superclass, and defining the other class as a derived class or subclass of Entry. For example, if we add an email field and responsibilities to Entry, the property of inheritance causes it to be added to all of the derived classes automatically.

In general, you can take an existing class A, the base class, and create from it a new class B, the derived class. The derived class B inherits all of the properties of its base class A. In particular, the data and operations defined for A are now also defined for B. (Notice the is-a relationship—every B is also an A.) The idea is then to specialize class B, usually by adding specific properties to those already inherited from A. To see how this process works, let's look at an example in C++.

images

Figure 15.4 A Hierarchy of Floor Types

Deriving One Class from Another Class

Let's solve the Entry problem with inheritance. That is, let's create class ExpandedEntry that has the ComparedTo function and the additional parameterized constructor. Here is the specification file for the new class:

class ExpandedEntry : public Entry
{
public:
  ExpandedEntry(Name newName, TimeOfDay newTime);
  // Creates an ExpandedEntry object with newName as the name
  // attribute and newEntry as the Entry attribute
  RelationType ComparedTo(ExpandedEntry otherEntry);
  // Post: Returns
  //       BEFORE if instance's time object is earlier than entry's
  //       SAME if they are identical
  //       AFTER if instance's time object is later than entry's
}

The statement

class ExpandedEntry : public Entry

states that ExpandedEntry is derived from Entry. The reserved word public declares Entry to be a public base class of ExpandedEntry. As a consequence, all public members of Entry (except constructors) are also public members of ExpandedEntry. In other words, Entry's member functions GetNameStr and GetTimeStr can also be invoked for ExpandedEntry objects.1 However, the public part of ExpandedEntry specializes the base class by adding the ComparedTo function and providing an additional constructor. This relationship is pictured in FIGURE 15.5.

Figure 15.5 shows that each ExpandedEntry object is an Entry object, and more. C++ uses the terms “base class” and “derived class” instead of “superclass” and “subclass.” The terms “superclass” and “subclass” can be confusing because the prefix sub usually implies something smaller than the original (for example, a subset of a mathematical set). In contrast, a subclass is often “bigger” than its superclass—that is, it has more data and/or functions.

Figure 15.5 includes arrows between the two ovals labeled GetNameStr and GetTimeStr. Because Entry is a public base class of ExpandedEntry, and because GetNameStr and GetTimeStr are not redefined by ExpandedEntry, the two functions available to clients of ExpandedEntry are the same as the ones inherited from Entry. We use the arrow between the corresponding ovals to indicate this fact. (Notice in Figure 15.5 that Entry's constructors are operations on Entry, not on ExpandedEntry. The ExpandedEntry class must have its own constructors.)

images

Figure 15.5 Class Interface Diagram for ExpandedEntry Class

Specification of the ExpandedEntry Class

Following is the fully documented specification of the ExpandedEntry class with the default constructor added. Notice that the preprocessor directive

#include “Entry.h”

is necessary for the compiler to verify the consistency of the derived class with its base class.

images

Implementation of the ExpandedEntry Class

When the default constructor of a derived class has no code, the default constructor for the base class is automatically called in its place. Because there are no new data fields in ExpandedEntry to be initialized, it makes sense to let the default constructor for Entry do the work for us. However, we will need a constructor initializer for the new parameterized constructor. We must take the name and time parameters and break them apart, because the constructor for the Entry class takes them as individual values.

ExpandedEntry::ExpandedEntry(Name newName, TimeOfDay newTime) :
   Entry(newName.GetFirstName(), newName.GetMiddleName(),
         newName.GetLastName(), newTime.GetHours(),
         newTime.GetMinutes(), newTime.GetSeconds())
{ }

The ComparedTo function must compare the time in the instance with the time in the parameter. Now, we have a problem: We don't have access to the time field in the Entry class from the ExpandedEntry class.

With C++, inheritance does not imply unrestricted access . Although a derived class inherits the members of its base class, both private and public, it cannot directly access the private members of the base class. FIGURE 15.5 shows the variables name and time to be encapsulated within the Entry class. Neither external client code nor ExpandedEntry member functions can refer to these two variables directly. If a derived class could access the private members of its base class, any programmer could then write code todirectly inspector modify the private data, thereby defeating the benefits of encapsulation.

The solution to the problem is to add a getter function to the base class—in this case, Entry. The derived class can then see the value in the field, without having access to it. To make Entry as widely usable as possible, we should add two getter functions: GetTime and GetName.

We use GetTime to access the time in the instance and the time in the parameter.

RelationType ExpandedEntry::ComparedTo(ExpandedEntry otherEntry)
{
  return (GetTime().ComparedTo(otherEntry.GetTime()));
}

This little Return statement illustrates two important concepts. First, the function GetTime by itself applies to the class instance. Second, to get the time in the parameter, GetTime must be explicitly applied to it. Because we are adding getter functions to the class, we can removethe functions that return the name and time as strings. Users would then have the tools toformat name and time themselves.

Following is the specification file for the revised Entry class.

images

Here is the revised implementation file:

images

This experience illustrates why an important aspect of object-oriented design is planning ahead for reuse of classes. If we had thought about Entry in more general terms when we first designed it, we would have included these getter functions from the outset. Inheritance makes it so easy to reuse classes by extending them that we almost always want to generalize our designs to maximize their potential for future reuse.

Now we can write the implementation file for ExtendedEntry. As you look at this file, notice how little additional code is required to extend the base class with this new functionality. That's precisely the point behind the use of inheritance in object-oriented programming.

images

We compile the file ExpandedEntry.cpp into an object code file—say, ExpandedEntry.obj. Next we write a test driver and compile it into test.obj. The development environment then creates an executable file by linking these two files with the existing Entry.obj file.

We're finally ready to test the resulting program. Here is the output from the same test driver that was used previously to test the revised Entry class in Chapter 13:

Here is a test driver and its output.

images

Output:

images

The remarkable thing about extending a base class with inheritance is that modification of the base class is unnecessary, assuming it has the necessary getter operations. If these had already been present in the Entry class, we could have implemented ExtendedEntry even without having access to the source code for Entry. The C++ compiler is able to get all of the information it needs from the specification file for the base class, without having to recompile its implementation file. The linker then joins the object code for the derived class with the existing object code for the base class to produce the executable file.

Further variations of the Entry ADT can be created in ways the creator never considered. Through classes and inheritance, OOP languages facilitate code reuse. A class such as Entry can be used without modification in many different contexts, or it can be adapted to a particular context by using inheritance. Inheritance allows us to create extensible data abstractions—a derived class typically extends the base class by including additional private data or public operations or both.

Constructor Execution Order

Having discussed both inheritance and composition, we can give a complete description of the order in which constructors are executed:
 

Given a class X, if X is derived from base class Y:

1. The base class (Y) constructor is executed first.

2. Constructors for X's member objects (if any) are then executed.

3. The body of X's constructor is executed last.
 

For example, when an ExtendedEntry object is created, the constructor for the Entry class is called first. This action triggers calls to the constructors for its Name and TimeOfDay members. After that, the body of Entry's constructor is executed, which is empty. Then the body of ExtendedEntry's constructor is executed, which also happens to be empty.

images

images

images

images

images

15.3 Dynamic Binding and Virtual Functions

Early in the chapter, we said that object-oriented programming languages provide language features that support three concepts: data abstraction, inheritance, and dynamic binding. Data abstraction is accomplished with the class construct; we have just covered inheritance. We now cover dynamic binding.

The term dynamic binding means, more specifically, “dynamic binding of an operation to an object.” To explain this concept, let's look at an example. Given the TimeOfDay and ExtTime classes described in this chapter, the following code creates four class objects and compares them using the ComparedTo function:

TimeOfDay time1(8, 30, 30);
TimeOfDay time2;
ExtTime time3(10, 45, 0, CST);
ExtTime time4;
RelationType result1 = time1.ComparedTo(time2);
RelationType result2 = time3.ComparedTo(time4);

This code fragment invokes two different ComparedTo functions, even though the functions appear to have the same name. The first function call invokes the ComparedTo function of the TimeOfDay class; the second call invokes the ComparedTo function of the ExtTime class. In this code fragment, the compiler uses static (compile-time) binding of the operation (ComparedTo) to the appropriate object. The compiler can easily determine which ComparedTo function to call by checking the data type of the associated object.

In some situations, the compiler cannot determine the type of an object, however, so the binding of an operation to an object must occur at run time. One such situation involves passing class objects as arguments.

The basic C++ rule for passing class objects as arguments is that the argument and its corresponding parameter must be of identical types. With inheritance, C++ relaxes this rule to a certain extent. You may pass an object of a derived class as an argument to a function where the parameter is the type of the base class, but not the other way around—that is, you cannot pass an object of a base class as an argument where the parameter is of the derived type. More generally, you can pass an object of a descendant class as an argument to a function where the parameter is of any of its ancestor classes. This rule has a tremendous benefit: It allows us to write a single function that applies to any descendant class instead of having to write a different function for each subclass.

For example, let's assume that both TimeOfDay and ExtTime have a Write function. We could write a fancy Print function that takes as an argument an object of type TimeOfDay or any class descended from TimeOfDay:

void Print(TimeOfDay time)
{
  cout << “*************************” << endl;
  cout << “** The time is “;
  time.Write();
  cout << endl;
  cout << “*************************” << endl;
}

Given the code fragment

TimeOfDay startTime(8, 30, 0);
ExtTime endTime(10, 45, 0, CST);
Print(startTime);
Print(endTime);

the compiler lets us pass either a TimeOfDay object or an ExtTime object to the Print function. Unfortunately, the output is not what we would like. When endTime is printed, the zone CST is missing from the output. Let's see why.

The Slicing Problem

Our Print function uses passing by value for the parameter time. Passing by value sends a copy of the argument to the parameter. Whenever you pass an object of a derived class to an object of its base class using the pass by value technique, only the data members they have in common are copied. Recall that a derived class is often “larger” than its base class—that is, it contains additional data members. For example, a TimeOfDay object has three data members (hours, minutes, and seconds), but an ExtTime object has four data members (hours, minutes, seconds, and zone). When the larger class object is copied to the smaller parameter using pass by value, the extra data members are discarded or “sliced off.” This situation is called the slicing problem (see FIGURE 15.7).

images

Figure 15.7 The Slicing Problem Resulting from Passing by Value

The slicing problem also occurs with assignment operations. In the statement

baseClassObject = derivedClassObject;

only the data members that the two objects have in common are copied. Additional data members contained in derivedClassObject are not copied.

With passing by reference, the slicing problem does not occur because the address of the caller's argument is sent to the function. To see how this approach works, let's change the heading of our Print function so that someEntry is a reference parameter:

void Print(TimeOfDay& time)

Now when we pass endTime as the argument, its address is sent to the function. Its ExtTime zone member is not sliced off because no copying takes place. But to our dismay, the Print function still prints only three of endTime's data members—hours, minutes, and seconds.

Within the Print function, the difficulty is that static binding is used in the statement

time.Write();

The compiler must generate machine language code for the Print function at compile time, but the type of the actual argument (TimeOfDay or ExtTime) isn't known until run time. How can the compiler know which Write function to use—TimeOfDay::Write or ExtTime::Write? The compiler cannot know, so it uses TimeOfDay::Write because the parameter time is of type TimeOfDay. Therefore, the Print function always prints just three values—hours, minutes, and seconds—regardless of the type of the argument.

Fortunately, C++ provides a very simple solution to the slicing problem: virtual functions.

Virtual Functions

Suppose we make one small change to our TimeOfDay class declaration: We begin the declaration of the Write function with the reserved word virtual.

images

Declaring a member function to be virtual instructs the compiler to generate code that guarantees dynamic (run-time) binding of a function to an object. That is, the determination of which function to call is postponed until run time. (Note that to make Write a virtual function, the word virtual appears in one place only—the TimeOfDay class declaration. It does not appear in the Write function definition that is located in the TimeOfDay.cpp file, nor does it appear in any descendant class—such as ExtTime—that redefines the Write function.)

Virtual functions work in the following way. If a class object is passed by reference to some function, and if the body of that function contains a statement

param.MemberFunc(…);]

then

1. If MemberFunc is not a virtual function, the type of the parameter determines which function to call. (Static binding is used.)

2. If MemberFunc is a virtual function, the type of the argument determines which function to call. (Dynamic binding is used.)

With the addition of just one word—virtual—the difficulties we encountered with our Print function disappear entirely. If we declare Write to be a virtual function in the TimeOfDay class, the function

images

works correctly for arguments of either type TimeofDay or type ExtTime. The correct Write function (TimeOfDay::Write or ExtTime::Write) is invoked because the argument includes the information needed at run time to choose the appropriate function. With this approach, deriving a new and unanticipated class from TimeOfDay presents no complications. If the new class redefines the Write function, then our Print function still works correctly. Dynamic binding ensures that each object knows how to print itself and that the appropriate version is invoked. In OOP terminology, Write is a polymorphic operation—an operation that has multiple meanings depending on the type of the object that responds to it at run time.

Here are some key points when using virtual functions in C++:

1. To achieve dynamic binding, you must use pass by reference when passing a class object to a function. If you use pass by value, the compiler does not use the virtual mechanism; instead, member slicing and static binding occur.

2. In the declaration of a virtual function, the word virtual appears only in the base class, not in any derived class.

3. If a base class declares a virtual function, it must implement that function, even if the body is empty.

4. A derived class is not required to provide its own reimplementation of a virtual function. In this case, the base class's version is used by default.

5. A derived class cannot redefine the function return type of a virtual function.

15.4 Object-Oriented Design

Now that we've explored the language features that let us implement an object-oriented design, we turn to the phase that precedes implementation—object-oriented design itself.

A computer program usually models some real-life activity or concept. For example, a banking program models the real-life activities associated with a bank. A spreadsheet program models a real spreadsheet, which was a large paper form used in the past by accountants and financial planners. Programs for some kinds of robots model human perceptions and motions.

Nearly always, the aspect of the world that we are modeling (the application domain or problem domain) consists of objects—checking accounts, bank tellers, spreadsheet rows, spreadsheet columns, robot arms, robot legs. OOD is based on the philosophy that programs are easier to write and understand if the major objects in a program correspond closely to the objects in the problem domain. The programmer then focuses on how best to represent the real-world objects of the problem domain using the data types and operations provided by the programming language (the solution domain).

Object-oriented design can follow many different paths. Different software engineering researchers advocate different techniques. Our purpose here is not to teach you one particular technique in great detail or to present a summary of all the techniques. Rather, we will introduce you to a straightforward four-stage process that captures the essence of OOD. The four stages are brainstorming, filtering, scenario exploration, and responsibility algorithm design.

Brainstorming is the stage in which we make a first pass at identifying the objects in the problem. We freely list every possible object we can think of, in both the problem and solution domains.

Filtering is the stage in which we review the objects proposed in the brainstorming stage. We look for objects that are redundant or that can be combined, and perhaps notice some that are missing. Each object that survives the filtering stage becomes a class in the solution and is recorded on a CRC card. CRC stands for classes, responsibilities, and collaborations. A CRC card is a convenient way of recording important features of a class interface that emerge as we proceed through the design.

images

Figure 15.8 A Blank CRC Card

Scenario exploration is the stage in which the behavior of each class is determined. Because a class is responsible for its own behavior, we call these behaviors responsibilities. (You've already seen some use of this terminology. Recall that we have collectively referred to the getter functions of a class as its “knowledge responsibilities.”) In this stage, we explore “what if” questions to be sure that we have examined all of the situations. When all of the responsibilities of each class have been determined, they are recorded on the class's CRC card. We also note the names of any other classes with which the class must collaborate (interact) to complete each responsibility.

In the last stage, responsibility algorithm design, we write the algorithms for each of the responsibilities outlined on the CRC cards. This stage is very similar to what happens in functional decomposition. However, in many cases the responsibility algorithms are so simple that further decomposition isn't necessary, and we can go directly to writing out the concrete steps. See FIGURE 15.8 for an example of a CRC card.

Although these techniques were originally developed for use in programming teams, we can apply them to our own individual thought processes as well. Now that you have a general idea of how this OOD technique works, we can examine the specifics of each of the stages.

Brainstorming

Just exactly what is brainstorming? The dictionary defines it as a group problem-solving technique that involves the spontaneous contribution of ideas from all members of the group.2 Brainstorming brings to mind a group of bright advertising people, tossing around ideas for a slogan for the latest revolutionary product. This picture seems at odds with the traditional concept of a computer programmer, slaving away for days in a cubicle, who finally jumps up, shouting, “It works!”

As computers have grown more powerful, the problems that can be solved with them have also grown increasingly more complex, and the picture of the solo coding genius has become obsolete. Solutions to complex problems need new and innovative solutions based on the creative collaborations of a diverse team.

In the context of object-oriented problem solving, brainstorming is a group activity designed to produce a list of candidate classes to be used to solve a particular problem. Just as the people brainstorming an advertising slogan know something about the product before the session begins, so brainstorming for classes requires that the participants know something about the problem. Each participant should be familiar with the requirements document and any additional information relating to the technical aspects of the project.

How does this process relate to individual problem solving? The equivalent step in the problem-solving process is “Understand the problem.” Ask questions; try rewriting the problem statement in your own words. Take a sheet of paper and start writing down possible classes. Look for nouns in the problem statement that may indicate objects in the problem domain. Think about how the user will interact with the program. Review the classes that are available in libraries to see if any might be useful in the solution domain (including through inheritance in an extended class).

When you have a list of possible classes, you can then move to the next stage: filtering.

Filtering

The filtering step produces a smaller list of classes that we believe are sufficient to solve the problem. As part of the filtering stage we may recognize alternative approaches, and choose one set of objects over another set.

For example, when we are developing a highway traffic management system, we may list managers, drivers, lights, and cars as potential objects. In filtering the list, we might see that the problem can be solved either by representing managers and drivers or by representing lights and cars—but we don't need both. We may also find that the C++ Standard Template Library contains objects that we can use to implement items on our list.

Some classes may not really belong in the problem solution. For example, if we are simulating a calculator, we might list the user as an object in the problem domain. However, the user is not within the simulation; the user is an entity outside the problem that provides input to the simulation. Another problem domain object is the on button. A little thought shows that the on button is not actually part of the simulation; it is represented by the fact that the user has started the simulation program running.

Scenario Exploration

The goal of the scenario exploration phase is to assign responsibilities to each class. The responsibilities are the tasks that each class must perform. As we have seen, responsibilities are eventually implemented as functions. At this stage we are interested only in what the tasks are, not in how they might be carried out. To talk about what an object does, we need to determine its internal state, referred to as its attributes. We do not need to determine how these attributes are to be represented at this stage, only what they are.

For example, our TimeOfDay class has three attributes: hours, seconds, and minutes. We have represented them as three integer values. In fact, we could keep the time as one integer value, seconds. GetHours and GetMinutes could then return these values by calculating them from seconds. Thus, at the logical (design) level, there are three values; at the implementation level, however, there may be only one value. The process we are describing is at the design (logical) level.

We distinguish two types of responsibilities: what a class must know about itself (knowledge) and what a class must be able to do (behavior). A class encapsulates its data (attributes); objects in one class cannot directly access data in another class. Encapsulation is a key to abstraction. However, each class has the responsibility of providing the information about itself that is needed by classes with which it collaborates.

For example, a Student class should “know” its name and address. The responsibilities for this knowledge might be called Get Name and Get Address. Whether the address is kept in the Student class or whether the Student class must ask some other class to access the address is irrelevant at this stage. The important fact is that the Student class is able to access and return (get) its own name and address.

The responsibilities for behavior look more like the tasks we described in functional decomposition. For example, a student object needs to calculate its GPA (grade-point average). In functional decomposition, we would say that a task is to calculate the GPA given the data. In OOD, we say that the student object is responsible for calculating its own GPA, perhaps by collaborating with an object that contains the student's grades.

This distinction is both subtle and profound. The final code for the calculation may be similar, but it is executed in different ways. In a procedural design, the main program calls the function that calculates the GPA, passing the student record as a parameter. In an object-oriented program, a message is sent to the student object to calculate its GPA. There are no parameters because the object to which the message is sent knows its own data.

How do we go about determining the responsibilities? The name for this phase gives a clue as to how we would assign responsibilities to classes. For our purposes, a scenario exploration is a sequence of steps that describes an interaction between a client (user or object) and any objects with which it must collaborate to satisfy a goal (which may include the main application).

Here is how the process works. The team uses play-acting to test different scenarios. During this exercise, each member of the team plays the role of one of the classes. Scenarios are “what if” scripts that allow participants to act out different situations. When a class has been sent a message, the actor holds up the CRC card and responds to the message, perhaps sending messages to others if necessary. As the scripts are being acted out, missing responsibilities are unearthed and unneeded responsibilities are detected. Sometimes the need for new classes surfaces. Although waving cards in the air when “you” are active might feel a little awkward at first, team members quickly get into the spirit of the action when they see how effective the technique is. See FIGURE 15.9.

The output from this phase is a set of CRC cards representing the core classes in the problem solution. The responsibilities for each class are listed on the card, along with the classes with which each responsibility must collaborate.

images

Figure 15.9 A Scenario Walk-through in Progress

Responsibility Algorithms

Eventually, the algorithms must be written for the responsibilities. Because we focus on data rather than actions in the object-oriented view of design, the algorithms for carrying out responsibilities tend to be fairly short. For example, knowledge responsibilities usually just return the contents of a variable in the object or send a message to another object to retrieve one of its attributes. Action responsibilities are a bit more complicated, often involving calculations. Thus the functional decomposition method of designing an algorithm is often appropriate for behavior responsibility algorithms.

A Final Word

To summarize, functional decomposition design methods focus on the process of transforming the input into the output, resulting in a hierarchy of tasks. By contrast, object-oriented design focuses on the data objects that are to be transformed, resulting in a hierarchy of objects. The nouns in the problem description become objects; the verbs become responsibilities. In a top-down design, the verbs are the primary focus; in an object-oriented design, the nouns are the primary focus.

The methodology that we have described makes use of the CRC card. This card is simply a notational device to help you organize your classes; its use is not a methodology.

images

Figure 15.10 Alternative Choices of Objects in an Air Traffic Control Application

15.5 Implementing a Design

In OOD, when we first identify an object, it is an abstract entity. We do not immediately choose an exact data representation for that object's attributes. Similarly, the responsibilities of objects begin as abstract operations, because there is no initial attempt to provide algorithms. Of course, eventually we have to implement the attributes and operations. For each abstract object, we must complete two tasks:
 

images Choose a suitable data representation for its attributes

imagesCreate algorithms for its responsibilities
 

To select a data representation for an object, the C++ programmer has three options:
 

1. Use a built-in data type.

2. Use or extend an existing class.

3. Create a new class.
 

For a given object, a good rule of thumb is to consider these three options in the order listed here. A built-in type is the most straightforward to use and understand, and operations on these types are already defined by the language. If a built-in type is not adequate to represent an object, you should survey the available ADTs in a class library (either the system's or your own) to see if any are a good match for the abstract object. Because we can use inheritance, it's not necessary to find an exact match, as long as an extension to the existing class will be sufficient. If no suitable ADT exists, you must design and implement a new ADT to represent the object.

In addition to choosing a data representation for the abstract object, we must implement the responsibility algorithms. With OOD, the algorithms for many responsibilities consist of a few concrete steps that can be directly translated into C++. Of course, this is not always the case. If an operation is complex, it may be best to treat the operation itself as a new problem and to use functional decomposition on the control flow. In this situation, it is appropriate to apply both functional decomposition and object-oriented methodologies together. Experienced programmers are familiar with both methodologies and often use them either independently or in combination with each other. However, the software development community is becoming increasingly convinced that although functional decomposition is important for designing low-level algorithms and operations on ADTs, the future in developing huge software systems lies in OOD and OOP.
 

Creating an Appointment Calendar

Problem: Create an application that allows the user to edit entries on a list representing an appointment calendar. The input file contains a list of Entry objects similar to those defined in Chapter 12; however, each Entry object also contains a Date member. The user enters a name, and the application displays the entry for that name. After that, the user chooses which field to change and enters a new value. At the end of the processing, the list of updated entries is then written back to the file.
 

Identifying Initial Classes: Here is the list of potential objects, based on the problem statement and some brainstorming:
 

Appointment Calendar

List

Use

Entry

Name

Time

Date

Input File

Output File

Format

Field
 

Some of these items are clearly not a part of our solution, such as the User. The purpose of brainstorming, however, is to generate ideas without any inhibitions. Once we've run out of ideas, we move on to filtering them.

Filtering: After brainstorming, we filter the classes. The discussion that takes place during the filtering stage may reveal some classes that are really the same thing. For example, Appointment Calendar, List, Input File, and Output File all refer to the same information. Appointment Calendar is another name for the contents of the file on which the data is stored. That file is input when the program starts up, and it is output before the program exits. The List is the name of the data structure into which the information is stored within the program. Thus we have two representations for the Appointment Calendar: the external file and the internal representation. So do we need an Appointment Calendar class? Let's leave it in the filtered list; we may remove it later.

We may also find classes that were overlooked. For example, even though the User isn't an object in the problem, its presence in the brainstorming list reminds us that we need a User Interface that takes care of input and output. In the design, we call it a class; however, it can be implemented as the main program.

Here is our filtered list, indented to show objects that are contained within other objects:
 

Appointment Calendar

User Interface

List

Entry

Name

Time

Date

File

Field

For each class that survives the filtering stage, we create a CRC card, which is just an index card with a line drawn down the middle. The name of the class is written at the top, and the two columns have the headings “Responsibilities” and “Collaborations.” If a class is involved in a hierarchical relationship, that fact is noted at the top of the CRC card.

We already have an Entry class composed of a Name class and a TimeOfDay class. We do not need to make CRC cards for them: They are available to us from our library. We can use inheritance to create an EntryWithDate class from either our Entry class or our ExpandedEntry class. The ComparedTo operation provided in the ExpandedEntry class compares the time field. It is logical to keep an appointment ordered by date, so let's have EntryWithDate inherit its attributes directly from Entry.

images

Scenario Walk-through: To further expand the responsibilities of the classes and see how they collaborate, we process the scenarios by hand. We ask a question such as “What happens when the user wants to change the name?” We then answer the question by observing how each object is involved in accomplishing this task. In a team setting, the cards are distributed among team members. When an object of a class is doing something, its card is held in the air to visually signify that it is active. If you're walking through the scenario by yourself, you may set the card off to one side or put it on a bulletin board.

What are the responsibilities of the User Interface and Appointment Calendar for getting the Entry and displaying it? Let's have the User Interface input the name and date and retrieve the appropriate Entry. To do so, it must ask the Appointment Calendar to provide the Entry, based on the name and date. That responsibility is written down on the card. Once the name and date are input, the User Interface must collaborate with the Name and Date classes to instantiate Name and Date objects, and then ask the AppointmentCalendar class to retrieve the appropriate entry from the list.

After the User Interface gets the entry to be edited, it must get the field to be changed and input that changed field. If the name is to be changed, the User Interface must input the new name and collaborate with the Name class to instantiate a Name object. The same procedure would be carried out if the other fields were changed. The User Interface then must collaborate with the Entry class to instantiate a new Entry object, which is sent to the Appointment Calendar to be inserted.

What is the role of the Appointment Calendar? It must search the List for an entry with a certain name and date. In fact, the AppointmentCalendar class should be responsible for entering the original List of entries and writing out the changed List of entries. Because our Entry hierarchy is a hierarchy of immutable objects, the object being changed must be removed from the List and the changed entry object then inserted.

images

Subsequent Scenarios: The preceding example illustrated using CRC cards in a single scenario. This particular scenario was selected from a list that was developed in a scenario brainstorming session. Here's a list of initial scenarios for our appointment calendar example:

What happens when the user
 

images Wants to change the date?

images Wants to change the name?

images Wants to change the address?

images Decides to not make any changes?

images Wants to change more than one field?

images Wants to change only one part of a field (such as the month)?

We walk through each scenario, adding responsibilities and collaborations to the CRC cards as necessary, and possibly extending the list of scenarios as we discover the need to do so (such as when we identify unanticipated error situations) or determining that we will not let the user perform that action. When every scenario, we can envision seems to be feasible with the existing classes, responsibilities, and collaborations. The design is then finished.

Attributes and Their Internal Representations: Because we have used classes and subprograms already in our library—Name, Time, Entry, ifstream, and ofstream—only the Date class needs to have its internal representation determined. Before we do that, however, we should determine the responsibilities for ADT Date.

Our problem statement tells us nothing about the Date class except that we need one. What sorts of operations will our ADT Date need to perform? Of course, we need a default constructor and a parameterized constructor that takes month, day, and year as parameters. We should also have observer operations that let us observe the values for the month, day, and year. Finally, we should have an operation that lets us compare two dates.

Type

Date

Domain

Each Date value is a single date after the year 1582 a.d. in the form of month, day, and year.
 

Operations

Construct a new Date instance.

Construct a new Date instance with a given month, day, year.

Inspect the date's month.

Inspect the date's day.

Inspect the date's year.

Compare two dates for “before,” “equal,” or “after.”

Here is the CRC card representing the Date ADT:

images

The domain of our ADT is the set of all dates after the year 1582 a.d. in the form of a month, a day, and a year. We restrict the year to be after 1582 a.d. so as to simplify the ADT operations (10 days were skipped in 1582 in switching from the Julian to the Gregorian calendar).

Default Constructor

For this operation, we use a C++ default constructor that initializes the date to January 1 of the year 1583.

Parameterized Constructor

The client must supply three arguments for this operation: month, day, and year. Although we haven't yet determined a concrete data representation for a date, we must decide which data types the client should use for these arguments. We choose integers, where the month must be in the range 1 through 12, the day must be in the range 1 through the maximum number of days in the month, and the year must be greater than 1582. Notice that these range restrictions will become the precondition for invoking this operation.

Inspect the Date's Month, Inspect the Date's Day, and Inspect the Date's Year: All three of these operations are knowledge responsibilities. They give the client access—albeit indirectly—to the private data. As in the past, we represent knowledge responsibilities as value-returning member functions with the following prototypes:

int GetMonth();
int GetDay();
int GetYear();

Compare Two Dates: This operation compares two dates and determines whether the first one comes before the second one, they are the same, or the first one comes after the second one. To indicate the result of the comparison, we use enumeration type RelationType with three values:

enum RelationType {BEFORE, SAME, AFTER};

Then we can code the comparison operation as a class member function that returns a value of type RelationType. Here is the function prototype:

RelationType ComparedTo(Date otherDate) const;

Because this is a class member function, the date being compared to otherDate is the class object for which the member function is invoked. For example, the following client code tests whether date1 comes before date2:

Date date1;
Date date2;
 .
 .
 .
if (date1.ComparedTo(date2) == BEFORE)
  DoSomething();

We are now almost ready to write the C++ specification file for our Date class. The simplest representation for a date is three int values—one each for the month, day, and year. Thus our internal structure is the same as the parameters. Here, then, is the specification file containing the Date class declaration:

images

And here is the implementation file:

images

EntryWithDate Class

The CRC card can be translated directly into the specification file for this class.

images

The algorithm for the ComparedTo function is a little complex. We are required to return an item of RelationType, but AppointmentCalendar is interested only in equality. If the name and the date are not equal, what do we send back: BEFORE or AFTER? In this case, let's use the comparison of the dates to determine the order.

images

AppointmentCalendar Class

Which type of list should we use: sorted or unordered? It really doesn't matter in this case. All we plan to do is insert, search, and delete entries. Let's use an ordered array-based list. We must, however, change the comparison operations to use the ComparedTo function rather than the relational operators. This information, along with the CRC card, gives us enough information to write the specification file.

images

Constructor: The constructor should read in the entries and insert them in the list. Because Appointment-Calendar will also write out the revised file, we must determine what the file looks like as well. There are three classes, each with three data values. Date's and TimeOfDay's data are integer values, while Name's data consists of strings. Let's write the integer values first followed by the strings. Which kind of loop shall we use to input the data? Because we always update the file after each session, let's have WriteListToFile put the number of entries as the first value on the file. This value can be used in a count-controlled loop to read in the entries.

AppointmentCalendar (In: ifstream)

Read number of entries
   FOR counter going from 0 to numberEntries - 1
    Read hours, minutes, seconds
    Read month, day, year
    Read first, middle, last
    TimeOfDay time(hours, minutes, seconds)
    Date date(month, day, year)
    Name name(first, middle, last)
    EntryWithDate entry(date, time, name)

Action Responsibilities: GetEntry takes a name and a date. The comparison operation used in the list compares only the name and date, but the object being compared must be an EntryWithDate. Thus, before we set up the iteration through the list, we must create a time object and then an EntryWithDate object using the time, name, and date.

GetEntry (In: name, date)
   Function return value: EntryWithDate

TimeOfDay time;
   EntryWithDate otherEntry(date, time, name)
   Reset list
   Set entry to list.GetNextItem()
   WHILE entry.comparedTo(otherEntry) != SAME
      Set entry to list.GetNextItem()
   Delete entry from list
   Return entry

What happens if there is no entry that matches the given date and time? In Chapter 16 we introduce a construct to handle such a case: exceptions and the try-catch statement. For the moment, we make it a precondition that such an entry exists. We then add an IsThere function to class AppointmentCalendar, thereby giving the client a tool to guarantee the precondition.

InsertEntry just calls the Insert operation for the list. The WriteListToFile operation is the inverse of the constructor.

WriteListToFile (In: ofstream)

Reset the list
   Write list.GetLength()
   WHILE list.HasNext()
     Set entry to next item
     Set time to entry.GetTime()
     Set name to entry.GetName()
     Set date to entry.GetDate()
     Write time.GetHours(), time.GetMinutes(), time.GetSeconds()
     Write date.GetMonth(), date.GetDay(), date.GetYear()
     Write name.GetFirstName, name.GetMiddleName, name.GetLastName()

Here is the implementation file for class AppointmentCalendar:

images

images

UserInterface Class (Implemented as main)

The UserInterface class is responsible for prompting for and inputting the name and date of the entry to be updated, then asking AppointmentCalendar to retrieve the entry. Once the entry is found and printed, UserInterface must ask the client which field is to be changed. Here we have a decision to make: Should we let the client change individual fields within the name, time, or date? For now, let's just have the client replace a complete name, time, or date.

We can create a menu of three characters: 'T' for time, 'D' for date, and 'N' for name. Once the client has indicated which field is to be changed, UserInterface prompts for and inputs that field, creates an EntryWithDate, and inserts the revised entry back into the list. At that point, UserInterface asks the client if he or she wants to continue.

UserInterface (implemented as main)

Define input file
     AppointmentCalendar calendar(inFile)
     Get name and date of entry to change
     DO
       IF calendar.IsThere(name, date)
          Set entry to calendar.GetEntry(name, date)
          Set time to calendar.GetTime()
          Print entry
          Set inputCode to GetInputCode()
          SWITCH (inputCode)
             'T' : GetTime()
             'D' : GetDate()
             'N' : GetName()
          EntryWithDate entry(date, time, name)
          calendar.InsertEntry(entry
        ELSE
          Write “No entry exists with this name and date”
        Ask client to enter 'Y' to continue or 'N' to stop
        Read yesOrNo
        IF toupper(yesOrNo) == 'Y'
           Get name and date of entry to change
     WHILE (toupper(yesOrNo) == 'Y')
     Close inFile
     Define outFile
     calendar.WriteListToFile(outFile)
     Close outFile

GetInputCode, GetEntryToChange, GetTime, GetDate, and GetName can be implemented as helper functions that prompt for and read the appropriate data. Here is the implementation file:

images

images

images

Here is the input file, followed by a scenario of three changes:

CalendarFile

images

Script from interactive session (client input is shaded):

images

Testing and Debugging

Testing and debugging an object-oriented program is largely a process of testing and debugging the C++ classes on which the program is built. The top-level driver also needs testing, but this testing is usually uncomplicated—OOD tends to result in a simple driver.

To review how to test a C++ class, refer back to the “Testing and Debugging” section of Chapter 9. There we walked through the process of testing each member function of a class. We made the observation then that you could write a separate test driver for each member function or you could write just one test driver that tests all of the member functions. The latter approach is recommended only for classes that have a few simple member functions.

When an object-oriented program uses inheritance and composition, the order in which you test the classes is, in a sense, predetermined. If class X is derived from class Y, you cannot test X until you have designed and implemented Y. Thus it makes sense to test and debug class Y (the base class) before you test class X.

When class Y contains an object of class Z, then you should generally test Z before Y. If you're waiting for someone else to finish the implementation of Z, however, you can still perform some initial tests on Y by writing a stub class for Z. A stub class provides a minimal implementation of the interface that allows a client class to operate. For example, if we were adding a function to determine the difference between two dates, we could code a stub that always returns BEFORE. That's sufficient to enable the client code to make the call and receive a value.

Stub classes also allow you to control the testing of a class more precisely, because you can write the stubs for the functions to return specific values. Why would that approach be helpful? When your class uses an object of a very complex class, it can be challenging to force the other class to return the values you need for testing. With a stub class, you have total—and very direct—control over what is returned to the class you are testing.

Testing and Debugging Hints

1. Review the Testing and Debugging Hints in Chapter 12. They apply to the design and testing of C++ classes, which are at the heart of OOP.

2. When using inheritance, don't forget to include the word public when declaring the derived class:

class DerivedClass : public BaseClass
{
 .
 .
 .
};

The word public makes BaseClass be a public base class of DerivedClass. As a consequence, clients of DerivedClass can apply any public BaseClass operation (except constructors) to a DerivedClass object.

3. The header file containing the declaration of a derived class must #include the header file containing the declaration of the base class.

4. Although a derived class inherits the private and public members of its base class, it cannot directly access the inherited private members.

5. If a base class has a constructor, it is invoked before the body of the derived class's constructor is executed. If the base class constructor requires arguments, you must pass these arguments using a constructor initializer:

DerivedClass::DerivedClass( … )
: BaseClass(arg1, arg2)
{
 .
 .
 .
}

If you do not include a constructor initializer, the base class's default constructor is invoked.

6. If a class has a member that is an object of another class and this member object's constructor requires arguments, you must pass these arguments using a constructor initializer:

SomeClass::SomeClass( … )
    : memberObject(arg1, arg2)
{
 .
 .
 .
}

If there is no constructor initializer, the member object's default constructor is invoked.

7. To achieve dynamic binding of an operation to an object when passing class objects as arguments, you must

images Pass the object by reference, not by value.

images Declare the operation to be virtual in the base class declaration.

8. If a base class declares a virtual function, it must implement that function even if the body is empty.

9.A derived class cannot redefine the function return type of a virtual function.

images Summary

Object-oriented design (OOD) decomposes a problem into objects—self-contained entities in which data and operations are bound together. In OOD, data is treated as an active, rather than passive, quantity. Each object is responsible for one part of the solution, and the objects communicate by invoking one another's operations.

The OOD process begins by identifying potential objects and their operations. Brainstorming is used to produce a list of possible classes in the problem. Filtering reexamines the classes, looking for duplicate classes, unnecessary classes, and missing classes. CRC cards are then written for the classes that survive the filtering phase. Next, scenarios are examined to determine the responsibilities of the classes. Finally, algorithms are written to carry out the responsibilities and concrete data representations are chosen.

Object-oriented programming (OOP) is the process of implementing an object-oriented design by using language mechanisms for data abstraction, inheritance, and dynamic binding (polymorphism). Inheritance allows any programmer to take an existing class (the base class) and create a new class (the derived class) that inherits both the data and the operations of the base class. The derived class then specializes the base class by adding new private data, adding new operations, or reimplementing inherited operations—all without examining or modifying the implementation of the base class in any way.

Dynamic binding of operations to objects allows objects of many different derived types to respond to a single function name, each in its own way. C++ supports dynamic binding through the use of virtual functions. Together, inheritance and dynamic binding have been shown to dramatically reduce the time and effort required to customize existing ADTs. The result is truly reusable software components whose applications and lifetimes extend beyond those conceived of by the original creator.

images Quick Check

1. How do programs based on functional decomposition differ from object-oriented programs in terms of the components that interact with one another to solve a problem? (pp. 756–757)

2. What are the three main facilities that are provided by an object-oriented language that support object-oriented programming? (pp. 756–757)

3. Is a virtual function bound statically or dynamically? (pp. 777–778)

4. Suppose you have a class called Phone and you want to extend it to support a country code, calling the new class InternationalPhone. How would you write the class heading for the new subclass? (p. 761)

5. Suppose that in Question 4, you wanted to create InternationalPhone as a separate class that contains an instance of Phone, rather than making it a subclass of Phone. Describe how you would accomplish this task. (p. 758)

6. What are the four major steps in designing an object-oriented solution to a problem? (pp. 778–779)

images Answers

1. In a structured program, functions interact with one another and with the data in the program. In an object-oriented program, objects (which bind operations and data together) are the components that interact. 2. Abstraction, inheritance, dynamic binding. 3. Dynamically. 4. class InternationalPhone : public Phone 5. By placing a member field within InternationalPhone that is of class Phone. 6. Brainstorming, filtering, scenarios, responsibility algorithms.

images Exam Preparation Exercises

1. Match the following terms with the definitions given below.

a. Structured (procedural) program

b. Object-oriented program

c. Inheritance

d. Superclass

e. Subclass

f. Composition

g. Static binding

h. Dynamic binding

i. Polymorphic

i. Determining, at run time, from which class to call a function.

ii.A class from which properties are acquired.

iii. A collection of classes designed using abstraction, inheritance, and polymorphism.

iv. An operation that has different meanings depending on its binding to an object.

v. Including an object of one class within another class.

vi. A class that acquires properties from another class.

vii. Determining, at compile time, from which class to call a function.

viii. A collection of functions, designed using functional decomposition.

ix. Acquiring the properties of another class.

2. Structured programming is better suited to developing small programs, whereas object-oriented programming is better for writing large programs. True or false?

3. Inheritance allows us to reuse functions from a base class and add new functions, but we cannot replace functions in the base class with new implementations. True or false?

4. To solve the slicing problem, we use a combination of pass by reference and virtual functions. True or false?

5. If we want a function to be virtual, where do we write the keyword: in the base class declaration file, in the base class definition file, in the derived class declaration file, or in the derived class definition file?

6. Which C++ language mechanism implements composition?

7. Suppose you have a subclass that contains several data members that are not defined in its superclass. What happens to those data members if you assign an object of the subclass to a variable of the superclass?

8. A client is supplied with the following declaration for a base class and a derived class:

class BaseClass
{
public:
  void PrintFields() const;
    .
    .
    .
};
class DerivedClass : BaseClass
{
public:
  void NewFunction();
  DerivedClass(int StartValue);
    .
    .
    .
};

The client writes the following code to call the constructor for an object of type DerivedClass, and then prints the fields in the newly created object.

DerivedClass anObject(10);
anObject.PrintFields();

The compiler reports an error for the second statement. What's wrong? How would you fix this problem?

9. Consider the following base and derived class declarations:

class BaseClass
{
public:
  void BaseAlpha();
private:
  void BaseBeta();
  float baseField;
};
class DerivedClass : public BaseClass
{
public:
  void DerivedAlpha();
  void DerivedBeta();
private:
  int derivedField;
};

For each class, do the following:

a. List all private data members.

b. List all private data members that the class's member functions can reference directly.

c. List all functions that the class's member functions can invoke.

d. List all member functions that a client of the class may invoke.

10. A class called DerivedClass is a subclass of a class called BaseClass. DerivedClass also has a member field that is an object of class ComposedClass.

a. Which class's constructor is called first when an object of class DerivedClass is created?

b. Which class's constructor is called last when an object of class DerivedClass is created?

11. Why does slicing occur with pass by value but not with pass by reference when a derived class object is passed to a parameter of its base class?

12. What's wrong with the following class declarations?

class BaseClass
{
public:
  virtual void BaseAlpha();
private:
  float baseField;
};
class DerivedClass : public BaseClass
{
public:
  virtual void BaseAlpha();
private:
  int derivedField;
};

13. Explain the difference between an is-a relationship and a has-a relationship.

14. When we code a derived class in separate files, which of the following steps do we take?

a. Include the base class specification and implementation file in both the derived class specification file and its implementation files.

b. Include the base class specification file in the derived class specification file, and include the base class implementation file in the derived class implementation file.

c. Include the base class specification file in the derived class specification file.

d. Include the base class implementation file in the derived class implementation file.

15. What is the task of the brainstorming phase?

16. What is the task of the filtering phase?

17. What is the task of the scenario exploration phase?

18. What is the task of the responsibility algorithm phase?

19. At which phase is a concrete data representation chosen?

images Programming Warm-Up Exercises

1. Given the following declaration for a TestScore class, write a derived class declaration called IDScore that adds an integer student ID number as a private member, and that supplies (1) a constructor whose parameters correspond to the three member fields, and (2) an observer that returns the ID number.

class TestScore
{
public:
  TestScore(string name, int score);
  string GetName() const;
  int GetScore() const;
private:
  string studentName;
  int studentScore;
};

2. Write the implementation file for the TestScore class in Exercise 1. The constructor just assigns its parameters to the private data members, and the observers simply return the corresponding member.

3. Write the implementation file for the IDScore class in Exercise 1. The constructor just assigns its parameters to the private data members, and the observer simply returns the corresponding member.

4. Write the specification file for a class called Exam that uses composition to create an array of 100 objects of class IDScore as defined in Exercise 1. The class can use the default constructor, and it will have a function that assigns an IDScore object to a location in the array, given the object and the location as parameters. The class should also have an observer that returns the IDScore object at the position specified by its parameter.

5. Write the implementation file for class Exam as defined in Exercise 4.

6. The following class represents a telephone number in the United States:

// SPECIFICATION FILE (phone.h)
enum PhoneType (HOME, OFFICE, CELL, FAX, PAGE);
class Phone
{
public:
  Phone(newAreaCode, int newNumber, PhoneType newType);
  void Write() const;
private:
  int areaCode;
  int number;
  PhoneType type;
}

Using inheritance, we want to derive an international phone number class, InternPhone, from the Phone class. For this exercise, we assume that the only change necessary is to add a country code (an integer) that identifies the country or region. The public operations of InternPhone are Write, which reimplements the Write function from the base class, and a class constructor, which takes four parameters corresponding to the four member fields in the class. Write the class declaration for the InternPhone class.

7. Implement the InternPhone class constructor as described in Exercise 6.

8. Implement the Write function of the InternPhone class as described in Exercise 6.

9. Write a global function WritePhone that takes a single parameter and uses dynamic binding to print either a U.S. phone number (Phone class) or an international phone number (InternPhone class). Make the necessary change(s) in the declaration of the Phone class from Exercise 6 so that WritePhone executes correctly.

10. Given the following declaration for a class that represents a computer in a company's inventory, write a derived class declaration (for a class called InstallRecord) that adds (1) a string field representing the location of the computer and (2) a field of class SimpleDate that holds the installation date. The new class should provide observers for each of the new fields. It should also reimplement the Write function.

class Computer
{
public:
  Computer(string newName, string newBrand, string newModel,
           int newSpeed, string newSerial, int newNumber);
  string GetName() const;
  string GetBrand() const;
  string GetModel() const;
  int GetSpeed() const;
  string GetSerial() const;
  int GetNumber() const;
  void Write() const;
private:
  string name;
  string brand;
  string model;
  int speed;
  string serialNumber;
  int inventoryNumber;
};

11. Implement the constructor for the Computer class declared in Exercise 10.

12. Implement the constructor for the InstallRecord class declared in Exercise 10.

13. Implement the Write function for the Computer class declared in Exercise 10. It should output each member field on a separate line.

14. Implement the Write function for the InstallRecord class declared in Exercise 10. It should output each member field on a separate line. Assume that the SimpleDate class provides a void function called Write() that outputs the date in a standard format.

images Programming Problems

1. Use object-oriented programming to develop a game application that simulates a roulette table. The roulette table has 36 numbers (1 to 36) that are arranged in three columns of 12 rows. The first row has the numbers 1 through 3, the second row contains 4 through 6, and so on. There is also a number 0 that is outside the table of numbers. The numbers in the table are colored red and black (0 is green). The red numbers are 1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, and 36. The other half of the numbers are black. In a simplified set of rules, players can bet on an individual number (including 0), the red numbers, the black numbers, the even numbers, the odd numbers, the numbers 1 to 18, the numbers 19 to 36, and any of the columns or rows in the table. The user should be allowed to enter one of the bets, and the application uses the rand function from <cstdl ib> as the basis for computing the number that would be rolled on the wheel. It then compares this number to the bet, and reports whether it won or lost. The process repeats until the user enters a quit command.

2. Use object-oriented programming to develop an extension to the application of Problem 1. The new application should allow the user to enter an initial amount of money into an account. In addition to placing a bet, the user specifies an amount to go with the bet. This amount is deducted from the account; any winnings are added to the account. The current winnings or losses (difference from the original amount) should be displayed in addition to the value of the account. Winnings are computed as follows:

Single-number bets pay 36 times the amount placed

Row bets pay 12 times the amount placed

Column bets pay 3 times the amount placed

Odd/even, red/black, and high/low half-bets pay 2 times the amount placed

The user should not be allowed to bet more than the amount in the account.

3. Use object-oriented programming to develop a game application that plays the children's game of rock, paper, scissors. The user enters a letter, indicating his or her choice. When a choice is entered, the rand function from <cstdl ib> is used to pick a value in the range of 1 through 3, with 1 corresponding to rock, 2 corresponding to paper, and 3 corresponding to scissors. The computer's choice is compared to the user's choice according to these rules: rock breaks scissors, scissors cut paper, paper covers rock. Choices that match are considered ties. Output a count of the wins by the user and the computer, and of the ties. The application ends when the user enters an invalid choice.

4. Use object-oriented programming to develop an extension to the application of Problem 3. The new application should accept either the original single letter or the full words (rock, paper, scissors). The capitalization of the words should not matter. The extended application should end only when the user enters “q” or “quit” as a choice and should prompt the user for one of the valid choices if an invalid value is entered.

5. Use the Computer and InstallRecord classes declared in Programming Warm-Up Exercises 10 to 14, together with the SortedList class developed in Chapter 13, as the basis for an object-oriented program that keeps track of a company's computer inventory. The company has at most 500 computers. The following operations should be supported:

Add a new computer to the list.

Delete a computer from the list.

Change the location of a computer.

Print a list of all computers in inventory-number order.

Print a list of all computers in a given location.

Print a list of all computers of a particular brand.

Print a list of all computers installed before a given date.

The application should keep the list sorted by inventory number. It should read an initial list from a file called original.dat. At the end of processing, it should write the current data in the list onto a file called update.dat in a format that could be read back in by the program as an initial list. You will also need to develop the SimpleDate class to the degree necessary to support the application.

images Case Study Follow-Up

1. The name of the input file is given explicitly in the case study code. Write a function that prompts for and inputs the file name. The file should be a parameter of the function. The function should also have a Boolean parameter through which it returns false if the input file cannot be found after giving the user three tries to enter a correct file name. Have it return true if the file is opened properly. Change the code in main to use this function.

2. Write and implement a test plan to verify that the change made in Exercise 1 is correct.

3. We have implemented the TimeOfDay object, using three values for hours, minutes, and seconds. Seconds are not relevant in this problem; they are too fine a distinction. Explore the idea of keeping the time of day as a single variable that represents the time as a number of minutes. What changes would be required in the client code of the case study if this change were made? Explain your answer.

4. Implement the alternative version of TimeOfDay using the scheme outlined in Exercise 3.


1 If a class declaration omits the word public and begins as

class DerivedClass : BaseClass

or if it explicitly uses the word private,

class DerivedClass : private BaseClass

2. Webster's New Collegiate Dictionary.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.138.226