64
PART ONE Problem-Solving Techniques he primary concern of the five chapters in Part I of this book is to develop a repertoire of problem-solving techniques that form the basis of the rest of the book. Chapter 1 begins by describing the characteristics of a good solution and the ways to achieve one. These techniques emphasize abstraction, modularity, and information hiding. The remainder of Part I dis- cusses data abstraction for solution design, C++ pointers and classes for use in implementations, and recursion as a problem-solving strategy. T ISBN: 0-558-13856-X Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

PART ONE Problem-Solving Techniqueswps.pearsoncustom.com/wps/media/objects/6904/7070186/CIS211_Ch… · and the various ways of approaching a problem. An Examination of Problem Solving

Embed Size (px)

Citation preview

PART ONE

Problem-Solving Techniques

he primary concern of the five chapters in Part I of this book is todevelop a repertoire of problem-solving techniques that form the basis

of the rest of the book. Chapter 1 begins by describing the characteristics ofa good solution and the ways to achieve one. These techniques emphasizeabstraction, modularity, and information hiding. The remainder of Part I dis-cusses data abstraction for solution design, C++ pointers and classes foruse in implementations, and recursion as a problem-solving strategy.

T

Carrano_5e.book Page 1 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Carrano_5e.book Page 2 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

3

CHAPTER

1

Principles of Programming and Software Engineering

his chapter summarizes several fundamental principlesthat serve as the basis for dealing with the complexities

of large programs. The discussion both reinforces the basicprinciples of programming and demonstrates that writing well-designed and well-documented programs is cost-effective.The chapter also presents a brief discussion of algorithmsand data abstraction and indicates how these topics relate tothe book’s main theme of developing problem-solving andprogramming skills. In subsequent chapters, the focus shiftsfrom programming principles to ways of organizing and usingdata. Even when the focus of discussion is on these new tech-niques, you should note how all solutions adhere to the basicprinciples discussed in this chapter.

1.1

Software Engineering and Object-Oriented Design

An Examination of Problem SolvingAspects of an Object-Oriented SolutionAbstraction and Information HidingPrinciples of Object-Oriented

ProgrammingObject-Oriented Analysis and DesignApplying the UML to OOA/DThe Software Life CycleIterative and Evolutionary DevelopmentRational Unified Process Development

PhasesWhat About the Waterfall Method of

Development?

1.2

Achieving a Better Solution

Evaluation of Designs and SolutionsOperation ContractsVerificationWhat Is a Good Solution?

1.3

Key Issues in Programming

ModularityStyleModifiabilityEase of UseFail-Safe ProgrammingDebuggingTesting

Summary

Cautions

Self-Test Exercises

Exercises

Programming Problems

T

CH01_5e.fm Page 3 Tuesday, June 27, 2006 5:06 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

4

Chapter 1 Principles of Programming and Software Engineering

1.1

Software Engineering and Object-Oriented Design

Where did you begin when you wrote your last program? After reading theproblem specifications and after the requisite amount of procrastination, mostnovice programmers simply begin to write code. Obviously, their goal is to gettheir programs to execute, preferably with correct results. Therefore, they runtheir programs, examine error messages, insert semicolons, change the logic,delete semicolons, pray, and otherwise torture their programs until they work.Most of their time is probably spent checking both syntax and program logic.Certainly, your programming skills are better now than when you wrote yourfirst program, but will you be able to write a really large program by using theapproach just described? Maybe, but there are better ways.

Realize that an extremely large software development project generallyrequires a team of programmers rather than a single individual. Teamworkrequires an overall plan, organization, and communication. A haphazardapproach to programming will not serve a team programmer well and willnot be cost-effective. Fortunately, a branch of computer science—

softwareengineering

—provides techniques to facilitate the development of computerprograms.

Whereas a first course in computer science typically emphasizes program-ming issues, the focus in this book will be on the broader issues of problemsolving. This chapter begins with an overview of the problem-solving processand the various ways of approaching a problem.

An Examination of Problem Solving

Here the term

problem solving

refers to the entire process of taking the state-ment of a problem and developing a computer program that solves that prob-lem. This process requires you to pass through many phases, from gaining anunderstanding of the problem to be solved, through designing a conceptualsolution, to implementing the solution as a computer program.

We will explore

object-oriented analysis and design

(OOA/D)

as aprocess for problem solving. From an object-oriented perspective, a

solution

isa computer program consisting of a system of interacting classes of objects. An

object

has a set of characteristics and behaviors related to the solution. Eachobject is responsible for some aspect of the solution. A set of objects havingthe same type is called a

class

. Object-oriented analysis and design techniqueshelp us to discover and describe these objects and classes. These techniquesgive us a starting place for moving from a problem statement to a solution.

Before embarking on an exploration of OOA/D techniques, we first willexplore several important aspects of object-oriented solutions.

Aspects of an Object-Oriented Solution

Unless otherwise stated, a solution to a problem is a computer program writtenin C++. A solution consists of

modules

, which are self-contained units of C++

Coding without a solution design increases debug-ging time

Software engineer-ing facilitates devel-opment of programs

A solution specifies a system of interact-ing objects

An object is an instance of a class

Carrano_5e.book Page 4 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design

5

code. A module could be a single, stand-alone

function

, a

method

of a class, aclass, several functions or classes that work closely together, or other blocks ofcode. Functions and methods implement algorithms. An

algorithm

is a step-by-step recipe for performing a task within a finite period of time. One actionthat an algorithm often performs is operating on a collection of data.

When designing a solution, your challenge is to create a good set of mod-ules. These modules must store, move, and alter data. They also use algorithmsto communicate with one another. When constructing a solution, you mustorganize your data collection so that you can operate on the data easily in themanner that an algorithm requires. In fact, most of this book describes ways oforganizing data.

This chapter will help you to design good solutions. One concept that isessential to creating a good solution for all but the smallest of problems isabstraction.

Abstraction and Information Hiding

When you design a modular solution to a problem, each module begins as abox that states what it does but not how it does it. No one box may “know”how any other box performs its task—it may know only what that task is. Forexample, if one part of a solution is to sort some data, one of the boxes will bea sorting algorithm, as Figure 1-1 illustrates. The other boxes will know thatthe sorting box sorts, but they will not know how it sorts. In this way, thevarious components of a solution are kept isolated from one another.

Abstraction

separates the purpose of a module from its implementation.Modularity and abstraction complement each other. Modularity breaks a solu-tion into modules; abstraction specifies each module clearly

before

you imple-ment it in a programming language. For example, what does the module

Modules implement algorithms, which often manipulate data

Specify what to do, not how to do it

sort

aBox

Unorganized data Data sorted intoascending order

I can sort datainto ascending order.

Sort this data forme; I don't care how

you do it.

The details of the sorting algorithm are hidden from other parts of the solution

FIGURE 1-1

Write specifications for each module before implement-ing it

Carrano_5e.book Page 5 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

6

Chapter 1 Principles of Programming and Software Engineering

assume and what action does it take? For what task is this module responsiblewhen called on? Such specifications will clarify the design of your solutionbecause you will be able to focus on the high-level functionality of your solu-tion without the distraction of implementation details. In addition, these prin-ciples allow you to modify one part of a solution without significantly affectingthe other parts. For example, you should be able to change the sorting algo-rithm in the previous example without affecting the rest of the solution.

As the problem-solving process proceeds, you gradually refine the boxesuntil eventually you implement their actions by writing C++ code—typically,classes and their methods. You should separate the purpose of a module fromits implementation. This process is known as

functional

(or

procedural

)

abstraction

.

Once a module is written, you can use it without knowing theparticulars of its algorithm as long as you have a statement of its purpose and adescription of its arguments. Assuming that the module is documented prop-erly, you will be able to use it knowing only its specifications. You will not needto look at its implementation.

Functional abstraction is essential to team projects. After all, in a team sit-uation, you will have to use modules written by others, frequently withoutknowledge of their algorithms. Will you actually be able to use such a modulewithout studying its code? In fact, you do each time you use a C++ StandardLibrary function, such as

sqrt

, as was noted earlier.Consider now a collection of data and a set of operations on the data. The

operations might include ones that add new data to the collection, removedata from the collection, or search for some data.

Data abstraction

focuses onwhat the operations do instead of on how you will implement them. The othermodules of the solution will “know”

what

operations they can perform, butthey will not know

how

the data is stored or

how

the operations are performed.For example, you have used an array, but have you ever stopped to think

about what an array actually is? You will see many pictures of arrays throughoutthis book. This artist’s conception of an array might resemble the way a C++array is implemented on a computer, and then again it might not. The point isthat you are able to use an array without knowing what it “looks like”—that is,how it is implemented. Although different systems may implement arrays in dif-ferent ways, the differences are transparent to the programmer. For instance,regardless of how the array

years

is implemented, you can always store thevalue 1492 in location

index

of the array by using the statement

years[index] = 1492;

and later write out that value by using the statement

cout << years[index] << endl;

Thus, you can use an array without knowing the details of its implementation,just as you can use the function

sqrt

without knowing the details of itsimplementation.

Specifications do not indicate how to implement a module

Specify what a module does, not how to do it

Specify what you will do to data, not how to do it

Carrano_5e.book Page 6 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design

7

Most of this book is about data abstraction. To enable you to thinkabstractly about data—that is, to focus on what operations you will perform onthe data and not on how the data is stored—you should define an

abstract datatype

, or

ADT

. An ADT is a collection of data

and

a set of operations on thedata. You can use an ADT’s operations, if you know their specifications, withoutknowing how the operations are implemented or how the data is stored.

Ultimately, someone—perhaps you—will implement the ADT by using a

data structure

, which is a construct that you can define within a programminglanguage to store a collection of data. For example, you might store the data ina C++ array of integers or in an array of objects or in an array of arrays.

Within problem solving, ADTs support algorithms, and algorithms arepart of what constitutes an ADT. As you design a solution, you should developalgorithms and ADTs in tandem. The global algorithm that solves a problemsuggests operations that you need to perform on the data, which in turnsuggest ADTs and algorithms for performing the operations on the data.However, the development of the solution may proceed in the opposite direc-tion as well. The kinds of ADTs that you are able to design can influence thestrategy of your global algorithm for solving a problem. That is, your knowl-edge of which data operations are easy to perform and which are difficult canhave a large effect on how you approach a problem.

Information hiding.

As you have seen, abstraction tells you to write func-tional specifications for each module that describe its outside, or public, view.However, abstraction also helps you identify details that you should hide frompublic view—details that should not be in the specifications but should be pri-vate. The principle of

information hiding

tells you not only to hide suchdetails within a module, but also to ensure that no other module can tamperwith these hidden details.

Information hiding limits the ways in which you need to deal with modulesand data. As a user of a module, you do not worry about the details of its imple-mentation. As an implementer of a module, you do not worry about its uses.

Principles of Object-Oriented Programming

Object-oriented programming languages allow us to build classes of objects. Aclass combines the

attributes

—or characteristics—of objects of a single typetogether with the objects’ operations—or

behaviors

—into a single unit.Attributes are typically data and the behaviors often operate on that data. InC++, classes specify the attributes and operations for the objects. The individ-ual data items specified in a class are called

data members

. The operationsspecified in the class are referred to as methods or

member functions

.

Encapsulation

is a technique that hides inner details. Whereas functionsencapsulate behavior, objects encapsulate data as well as behavior. For example,a clock encapsulates the time—an attribute—along with certain operations.You can request that the clock perform those operations but you cannot seehow it works.

An ADT is not a fancy name for a data structure

Develop algorithms and ADTs in tandem

All modules and ADTs should hide something

Objects encapsu-late attributes or data and behaviors or operations

Encapsulation hides inner details

Carrano_5e.book Page 7 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

8

Chapter 1 Principles of Programming and Software Engineering

You can see that encapsulation and abstraction are closely related. You willspend more time studying these concepts in Chapter 3, where C++ classes willbe introduced. In subsequent chapters, you will study various ADTs and theirimplementations as C++ classes. The focus will be on data abstraction andencapsulation.

Object-oriented programming

, or

OOP

, adds two more principles toencapsulation:

Classes can inherit properties from other classes. For example, once youhave defined a class of clocks, you can design a class of alarm clocks that inher-its the properties of a clock but adds operations to provide an alarm. You willbe able to produce an alarm clock quickly because the clock portion is done.Thus,

inheritance

allows you to reuse classes that you defined earlier—perhapsfor different but related purposes—with appropriate modification.

Inheritance may make it impossible for the compiler to determine whichoperation you require in a particular situation. However,

polymorphism

—which literally means

many forms

—enables this determination to be made atexecution time. That is, the outcome of a particular operation depends uponthe objects on which the operation acts. For example, if you use the + operatorwith numeric operands in C++, addition occurs, but when you use the

over-loaded operator

+ with strings, concatenation occurs. Although in this simpleexample the compiler can determine the correct meaning of +, polymorphismallows situations in which the meaning of an operation is unknown until exe-cution time. Chapter 8 discusses inheritance and polymorphism further.

You now have enough background to embark on an exploration of object-oriented analysis and design.

Object-Oriented Analysis and Design

Analysis

is the process of developing an understanding of what the problem isand what the requirements of a solution are. The

requirements

of a solutiongive the programmer a description of

what

a solution must be and

what

a solu-tion must do—without imposing

how

to design or implement that solution.Requirements give you a way of thinking about and understanding specific

Three Principles of Object-Oriented Programming1. Encapsulation: Objects combine data and operations.2. Inheritance: Classes can inherit properties from other classes.3. Polymorphism: Objects can determine appropriate operations at exe-

cution time.

KEY CONCEPTS

Inheritance sup-ports reusing software

Polymorphism allows an object’s behavior to be determined at runtime

An overloaded oper-ator has multiple meanings

Analysis explores a problem, not a solution

Carrano_5e.book Page 8 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design

9

parts of a problem. Analysis involves generating an accurate understanding ofwhat end users will expect the solution to be and do.

During object-oriented analysis (OOA), an understanding of the problemand the requirements of a solution are expressed in terms of the objects withinthe domain of the problem. The objects within the domain may represent real-world objects, software systems, or ideas. Using OOA, you describe theseobjects and their interactions among one another. It is important to note thatanalysis work involves discovery associated with understanding the problem.The process of doing analysis does not involve thinking about a solution to theproblem, but of thinking about the problem itself.

Design is the process of discovering an understanding of a solution to theproblem that fulfills the requirements discovered during analysis. The results ofanalysis act as input to the process of design. Object-oriented design (OOD)involves describing a solution in terms of software objects and how thoseobjects will collaborate with one another to fulfill the requirements of theproblem. Objects collaborate when they send messages to one another; that is,they call each other’s operations. Solutions to interesting problems have multi-ple objects collaborating with one another. The interactions among objects areas important as the objects themselves and require discovery. To solve theproblem efficiently, the collaborations among objects should be meaningfuland minimal.

During OOD, you typically create one or more models of a solution forthe problem. Some of the models will emphasize the interactions amongobjects; others will show the relationships among the objects. Taken together,the models create a design that can be implemented in C++ or any otherobject-oriented language.

Applying the UML to OOA/DThe Unified Modeling Language, or UML, is a modeling language used toexpress object-oriented designs. The UML provides specifications for manytypes of diagrams and text-based descriptions of various aspects of a design.The diagrams are particularly useful in showing specific, interesting aspects ofthe elements in the domain of the problem.

A use case is a set of textual scenarios or stories of the solution being usedthat describe the proposed solution. A use case is not object-oriented, but it isan important part of the analysis of the problem. A single use case usually hasseveral scenarios. The main success scenario (or “happy path”) describes howthe system satisfies the goals of the user when everything goes smoothly. Otheralternate scenarios describe the interaction between the user and the systemwhen specific things do not go well or when exceptional conditions apply.

For example, suppose that a customer wants to withdraw money from hisor her checking account. A main success scenario might look like this:

Customer makes request to withdraw money from a bank account.Bank identifies and authenticates the customer. Bank gets from thecustomer the type of account (savings or checking), the account

Object-oriented analysis explores a problem in terms of its objects

Design explores a solution to a problem

Object-oriented design explores a solution’s objects and their collaborations

The Unified Model-ing Language visu-ally represents object-oriented solu-tions as diagrams

A use case is a textual story

A scenario describes the sys-tem’s behavior under certain cir-cumstances from the perspective of the user

An example of a main success scenario

Carrano_5e.book Page 9 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

10 Chapter 1 Principles of Programming and Software Engineering

number, and the amount of the withdrawal. Bank verifies that theaccount balance is greater than the amount of the withdrawal. Bankgenerates a receipt for the transaction. Bank counts out the correctamount of money for the customer. Bank gives the customer thereceipt and money. Customer leaves the bank.

A scenario is written from the perspective of the user of the system. In this way,the analysis will focus on the responsibilities of the system to meeting a user’sgoals. That is, analysis will focus on what the system needs to accomplish tomeet the goals of its users.

Notice that the scenario does not describe how the system works. Forexample, “Bank identifies and authenticates the customer,” does not indicatehow this will occur. The focus should be on the interactions between the userand the system but not on the user interface to the system, which will beexplored during design.

Alternate scenarios should be written only for stories that are interesting ortricky. For example, one such scenario describes when the customer failsauthentication:

Customer makes request to withdraw money from a bank account.Bank identifies, but fails to authenticate the customer. Bank refuses toprocess the customer request. Customer leaves the bank.

Other alternate scenarios could include the denial of a withdrawal requestbecause insufficient funds are in the account or because the bank does not havesufficient cash on hand.

From the scenarios in the use case, you generate a list of objects by listingthe nouns in the use case. (This is where the analysis becomes object-oriented.)For example, bank, money, customer, account, account type, account number,transaction amount, and receipt are all found in the main success scenario. Thefinal solution might not use all of the objects discovered.

UML sequence diagrams. After you create a use case and list potentialobjects, you need to explore how these objects will interact with one another.You accomplish this by using UML sequence diagrams that model your usecase scenarios. A UML sequence diagram, or interaction diagram, showshow two or more objects will interact with one another over time within asingle scenario. This diagram allows you to visualize the messages sent among

An example of an alternate scenario

Object-Oriented Analysis and the Use Case1. Describe the problem domain in terms of scenarios involving the solu-

tion that satisfies user goals.2. Discover noteworthy objects, attributes, and associations within the

scenarios.

KEY CONCEPTS

A UML sequence diagram shows the interactions among objects over time

Carrano_5e.book Page 10 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design 11

the objects in a scenario and the order in which those messages happen. Thediagram gives a sense of the flow of the logic. This diagram is important whendefining the responsibilities of the objects: What must the object “remem-ber,” or keep track of, and what must the object do for other objects? Many ofthe responsibilities are described in the use case. Figure 1-2 shows a UMLsequence diagram for the main success scenario given previously.

Although the development of the use case is part of OOA, the creation ofsequence diagrams is part of OOD. The sequence diagram shows a scenario’sobjects from left to right horizontally and their actions in order of occurrencevertically. The UML represents an object as a square-cornered box containing

:Customer bank:Bank

If balance is greaterthanamountnewBalance = balance - amount

:Account

authorize(name, identifier)

OK

withdraw(accountNum, amount)

generateReceipt()

countCash()

cash, receipt

getBalance(accountNum)

balance

setBalance(newBalance)

Sequence diagram for the main success scenario

FIGURE 1-2

A UML sequence diagram represents an object as a box

Carrano_5e.book Page 11 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

12 Chapter 1 Principles of Programming and Software Engineering

the name of the object—if it has one—followed by a colon and the type of theobject, all underlined. In Figure 1-2, the object of type Bank is named bank,and the objects of type Customer and Account are nameless.

Each object in a sequence diagram has a lifeline denoted by a dashed linebelow the object box extending vertically and forward in time. Each lifelinecan have one or more activation bars, each represented by an open box onthe lifeline. An activation bar indicates when an object is active—and repre-sents a responsibility of the object’s class. The class needs a method to handlethat responsibility. To represent the sending of a message between objects, youdraw a solid arrow from the activation bar of the calling object to the top of anew activation bar on the lifeline of the called object. Such arrows are labeledwith a message expression indicating what task the object needs to perform.The message expression corresponds to the name of a method for handlingthat task. The message expression may also include parameters necessary forperforming the task. Ultimately, these parameters will correspond to methodarguments. When an object finishes a task, the activation bar associated withthat task ends. If the task returns something of interest to the calling object, adashed arrow may be shown pointing back to the calling object’s activationbar. You can label this arrow with the name of the variable receiving thereturned value.

In Figure 1-2, the customer object asks the bank for authorization bygiving its name and identifier—passed as arguments—to the authorize mes-sage. The bank then processes this message—as indicated by the activation barassociated with the bank object’s lifeline—and then signals whether or not thecustomer has been authenticated by returning a value to the variable OKwithin the Customer object. Note that you should write the messages in a gen-eral, language-independent fashion. A Bank class could declare the authorizemessage as follows:

class Bank{public: ... bool authorize(string name, string identifier); ...}; // end Bank

An object may also send messages to itself, as is done in the generate-Receipt and countCash messages in Figure 1-2. Notice the piggybacked acti-vation bars, which indicate that the Bank class is calling its own memberfunctions.

A sequence diagram can also show the creation of a new object. In Figure1-3, a new account is created. This diagram can represent a part of an alternatescenario in a use case, where a customer comes into the bank, but does not yethave an account. In the UML, «create» is called a stereotype. Stereotypes useleft and right guillemets (« and »), but when drawing a sequence diagram by

An activation bar on an object’s lifeline represents a responsbility of the object’s class

A stereotype identi-fies a spcial charac-teristic of an element

Carrano_5e.book Page 12 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design 13

hand, you can simply label the message arrow with create. A stereotype is usedto identify a unique characteristic of an element in any UML diagram.

The UML sequence diagram is a design document that gives guidance onseveral levels when implementing a solution. It gives a view of how the objectswill dynamically interact (collaborate) to solve a use case scenario. In particu-lar, a sequence diagram shows

■ Which objects collaborate to solve a particular use-case scenario.

■ The synchronous nature of the messages that are passed between the objectsin the scenario, providing the order in which tasks are accomplished.

■ Which object is responsible for what task or tasks by handling a particularmessage.

Sequence diagram showing the creation of a new object

FIGURE 1-3

bank:Bank

account:Account«create»

UML Sequence Diagram1. Visual representation of the flow of messages sent between objects

during a single scenario.2. Indicates the responsibilities of an object.3. Indicates the collaborations among objects.

KEY CONCEPTS

Carrano_5e.book Page 13 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

14 Chapter 1 Principles of Programming and Software Engineering

UML class diagrams. The last design element we will introduce is the UMLclass diagram, or static diagram. The class diagram shows the attributes andoperations of an individual class and how multiple classes are related to oneanother. These design elements do not change after they have been createdwithin a solution, that is, they are static, or unchanging.

The class diagram shown in Figure 1-4 represents a class of banks. Eachdiagram consists of a square-cornered box containing the name of the classcentered within its borders. Figure 1-4a shows the simplest of class diagrams: aclass with a name, but no attributes or operations. Figures 1-4b and 1-4c showthe bank’s attributes in the middle section and its operations in the bottomsection. If a class has no attributes, as in Figure 1-4c, an empty middle sectionmust be present.

The class diagram represents a conceptual model of a class of objects in a lan-guage-independent way. That is, the diagram does not dictate how the class isimplemented. It just gives you a design to work from. However, you can specifymany qualities associated with the attributes and operations of the class as anoption. Because these qualities are easily implemented in C++, and they increase

A UML class diagram shows the unchanging relat-ships among classes of objects

BanknameroutingNumauthorize(name,identifier)createAccount()

Bank

Bank

createAccount()

Three possible class diagrams for a class of banks

FIGURE 1-4

(a)

(b)

(c)

Carrano_5e.book Page 14 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design 15

the amount of information given in a design, the class diagrams in this text willinclude them.

The UML syntax for the attributes of a class is

[visibility] name [: type] [= defaultValue] [{property}]

where

■ All elements within square brackets are optional.

■ visibility can be + for public accessibility, - for private accessibility, or # forprotected accessibility. (Chapter 8 covers protected accessibility.) If omit-ted, the visibility defaults to private accessibility.

■ name is the name of the attribute.

■ type is the data type of the attribute.

■ defaultValue is the default value of the attribute.

■ property indicates some property that applies to this attribute.

At a minimum, the name of an attribute should be given. The defaultValue isgiven only in situations where the design dictates that a default value is appro-priate for that attribute. In certain cases it might be appropriate to omit thetype of an attribute, leaving this detail until the implementation phase.

The UML specifies that programming-language-independent names beused for the type of an attribute. This text will follow the UML recommenda-tions by using integer for integral storage, float for floating-point storage,boolean for boolean storage, and string for storing strings.

The property of an attribute can have one of the following values:

■ changeable indicates a normal, modifiable attribute and is usuallyomitted.

■ frozen indicates a constant or write-once attribute.

The attributes for the Bank class in Figure 1-4 could now be written as:

–name:string–routingNum:integer

The visibilities of these attributes are private and adhere to the object-orientedprogramming principle of information hiding.

The UML syntax for a class’s operations is more involved:

[visibility] name ([parameterList]) [: type] [{property}]

where

■ All elements within square brackets are optional.

■ visibility is the same as specified for attributes, except if omitted opera-tions default to public accessibility.

UML syntax for the attributes of a class

UML syntax for the attributes of a class

Carrano_5e.book Page 15 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

16 Chapter 1 Principles of Programming and Software Engineering

■ name is the name of the operation.

■ type is the data type of the result returned from the operation. If the opera-tion returns nothing, type can be either omitted or specified as void. Thesame language-independent types apply here as for attribute types.

■ property indicates some property that applies to this operation. The onlyproperty relevant to us is query, which indicates that the operation will notmodify any of the data members in the calling object.

■ parameterList is either empty—in which case the parentheses are stillrequired—or contains a comma-delimited list of parameters to the opera-tion. The syntax for each parameter looks like

[direction] name: type [= defaultValue]

where

� All elements within square brackets are optional.

� direction shows whether the parameter is used for input (in), output (out), or both input and output (inout).

� name is the name of the parameter.

� type is the data type of the parameter. The same language-independent types apply here as for attribute types.

� defaultValue is the value of this parameter if no corresponding actual argument is provided when this operation is called.

In Figure 1-4, the Bank class operations could be written as

+authorize(in name:string, in identifier:string): void {query}-createAccount()

The authorize operation has public accessibility and the createAccount opera-tion is private. Because the design of the authorize operation has the queryproperty specified, this operation will not change any of the data in the callingobject.

The UML provides for several types of relationships among the classes in aclass diagram. For example, Figure 1-5 shows a static (class) diagram for abanking system. Because the bank has both checking accounts and savingsaccounts for its customers, the diagram shows five classes of objects: a Bankclass, a Customer class, an Account class, a Checking class, and a Savings class.These classes collaborate by sending each other messages. Through these col-laborations, the classes are related to one another in various ways.

For example, the Bank and Customer classes have an association relation-ship indicated by the solid line with no arrowheads. The numbers at the endsof the association—and other relationships—are called multiplicities and areoptional. Each bank object is associated with zero or more customers, but each

An association indi-cates the classes know about each other

Carrano_5e.book Page 16 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design 17

customer is associated with one bank. Each customer can have multipleaccounts of any type, but an account can belong to only one customer.

The Bank and Account classes are in one kind of part/whole relationshipcalled an aggregation, or containment, relationship that is denoted by theopen diamond arrowhead near the containing class. In an aggregation relation-ship, the lifetime of the containing object and the object contained are notnecessarily the same. Banks “live” longer than the accounts that they contain.Another part/whole relationship, called composition, uses a filled-in diamondarrowhead near the containing class. This relationship denotes a stronger con-tainment, where the lifetimes of the container and the contained objects arethe same. For example, a ballpoint pen is a pen that has a ball at the tip. Whenthe pen “dies,” the ball “dies” with it. Thus, the pen and its ball are in a com-position relationship. By also looking at the multiplicities, we can say that abank stores—that is, contains—zero or more accounts. The Account andCustomer classes also have an aggregation relationship.

Bank-name:string-routingNum:integer

-createAccount()

Account-accountNum:integer-balance:float+getBalance():float {query}+withdraw(in accountNum:integer, in amount:float):boolean +deposit(in accountNum:integer, in amount:float):boolean

Checking-chargePerCheck:float -numCheck:integer -minBalance:float

+getBalance():float {query}

Savings-interestRate:float

-accrueInterest()+getBalance():float {query}

Customer-name:string-address:string+getName():string {query}+changeName(in newName:string)+getAddress():string {query}+changeAddress(in newAddress:string)

1

0..*

10..*

1

0..*

A UML class diagram of a banking system

FIGURE 1-5

Aggregation indi-cates that one class contains an instance of another class

Composition is a stronger form of aggregation; the life-times of the con-tainer and the contained objects are the same

Carrano_5e.book Page 17 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

18 Chapter 1 Principles of Programming and Software Engineering

Each of the bank’s accounts stores its account number and current balanceand allows a customer to ask for the current balance, withdraw funds, anddeposit funds. The Account class specifies these attributes and operationscommon to all accounts. The Account class is related to the Checking andSavings classes through a generalization relationship, indicated by the opentriangular arrowhead pointing to the general (or parent) class. The generaliza-tion relationship denotes inheritance. The attributes and operations of theancestor class, Account, are inherited by the descendant classes, Checkingand Savings.

Note that the Account class name is in italic in Figure 1-5. This denotesan abstract base class. The getBalance operation in the Account class is alsoin italic. This indicates that getBalance is not implemented in the Accountclass but must be implemented within the descendant class. In fact, the Check-ing and Savings classes each implement the inherited getBalance operation.This operation will take on polymorphic behavior when called from a parentclass object. Many of these points, both in design and implementation, will becovered in detail in Chapter 8.

Keeping the UML in perspective. Although the UML can be thought of as atool for documenting a solution to a problem, this is not how it should beused. Rather, you should use the UML to quickly draw various aspects of thecurrent problem to better understand it during OOA and to create a set ofobjects during OOD that can collaborate to solve this problem. Often smallteams of programmers will draw several UML diagrams on a white board and,after working out the kinks in the diagrams, take digital pictures of them forfuture reference. These design diagrams are considered an artifact, or docu-ment, associated with the analysis and design. However, the ultimate imple-mentation—that is, the solution—might not exactly match the original design.Thus, the design diagrams might not document the solution.

The UML is a useful tool for creating a design for a solution. It enablesprogrammers to explore and understand the problem domain and to develop adesign that is close to the required solution. Although the UML will not nec-essarily make better designs than other techniques, it can conceptually model aproblem domain in terms of software objects, independently of a programminglanguage. Because the UML gives a visual representation of the proposedsystem, humans—as visual creatures—can benefit from using it. After all, apicture is worth a thousand words. The UML enables the members of a pro-gramming team to communicate visually with one another, thereby gaining acommon understanding of the system that is being built.

Generalization indi-cates a family of classes related by inheritance

UML Class (Static) Diagram1. Shows the attributes and operations of individual classes.2. Shows the unchanging relationships among the classes in the solution.

KEY CONCEPTS

UML diagrams are not documentation

The UML is a tool for exploration and communication

Carrano_5e.book Page 18 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design 19

The Software Life CycleNow that we have several powerful analysis and design tools in our tool belt,let’s proceed to an examination of what a major piece of software “experi-ences” during its lifetime. The life cycle of software describes the phasesthrough which software will progress from conception to replacement to dele-tion from a hard disk forever. We will look at the development phases, fromproject conception to deployment to end users. Beyond this development pro-cess, software will need maintenance to correct errors and extend it to do newtasks. Eventually the software will be retired.

The development of good software is a complex affair. Over the last severaldecades many techniques have been developed to try to control that complex-ity. Some of these techniques have been more successful than others.

We will introduce the widely used Rational Unified Process (RUP) as abasis for examining the phases of the development process. The RUP uses andwill give structure to the OOA/D tools you learned earlier. As the RUP is alarge and complex process, most of its workings are beyond the scope of thistext. Thus, we will simply introduce the process.

Iterative and Evolutionary DevelopmentAn iterative development of a solution to a problem progresses throughmany short, fixed-length iterations, where each iteration cycles through theanalysis, design, implementation, testing, and integration of a small portion ofthe problem domain. The early iterations build the core of the system. Subse-quent iterations build on that core. After many iterations the entire solutionhas been developed.

Each iteration is assigned a predetermined duration—called a timebox—atthe beginning of the project. Typical timeboxes are two to four weeks inlength. The portion of the system that can be developed in less than two weeksis typically too small to be meaningful. In more than four weeks, the partialsystem becomes too large. At the end of each iteration, the partial systemshould be functional and completely tested. Each iteration makes relatively fewchanges to the partial system produced by the previous iteration, so it canoccur quickly. Thus, you do not expend too much effort on something thatyou may have misunderstood about the system requirements.

This process creates the solution incrementally. Thus, there are manyplaces for end users and developers to check that it solves the correct problem.Because each iteration produces a functional but partial system, developers canengage end users to generate feedback. This feedback influences the next itera-tion and can change the direction of development so a more correct solution isreached. Figure 1-6 illustrates the progress of an iterative development. We seethat early iterations may be way off base, reflecting the fact that the require-ments of the solution are not clearly understood. This is not anyone’s fault;even the people that have requested the software to be built, the domainexperts, may not be able to see what is required in the final system. Later itera-tions will probably not vary so much, as the requirements of the final system

The software life cycle describes the phases of software development

RUP gives structure to the software development process

Iterative develop-ment forms a solu-tion after many iterations; each iter-ation builds on the previous iteration until a complete solution is achieved

A critique of each iteration influences the next iteration

Carrano_5e.book Page 19 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

20 Chapter 1 Principles of Programming and Software Engineering

have become more stable and in line with the correct solution. In an incre-mental fashion, the correct solution is created.

Iterative, incremental development gives a programmer the ability to makethe requirements analysis and design evolve to meet a changing environment.Thus, the process is sometimes called evolutionary development.

Rational Unified Process Development PhasesThe RUP organizes development into four phases:

■ Inception—feasibility study, project vision, rough time and cost estimates.

■ Elaboration—refined project vision, iterative development of core system,development of system requirements, more accurate time and costestimates.

■ Construction—iterative development of remaining system.

■ Transition—testing and deployment of the system.

Figure 1-7 shows how the RUP development phases relate to iterations. Let uslook at the characteristics of each phase.

Inception phase. During the inception phase, we define the scope of the projectand decide whether the project is feasible. If a team has already implemented

System underdevelopment

Oneiteration

Correctsolution

Iterative development’s progress toward the correct system

FIGURE 1-6

Iterative develop-ment enables you to adapt a solution to a changing environment

Iterative and Evolutionary Development1. Determines timebox lengths at the start of a project.2. Keeps timebox lengths short (two to four weeks).3. Gets end-user and domain-expert feedback from iteration n to influ-

ence the direction of iteration n + 1.

KEY CONCEPTS

Carrano_5e.book Page 20 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design 21

solutions for similar problems, and the solution is clearly feasible, the inceptionphase may be very brief.

At a minimum, the inception phase must define an initial set of systemrequirements and choose the iteration timebox length. While defining theinitial requirements, you generate a core set of use case scenarios and deter-mine which elements of the solution involve the greatest risk. This set of sce-narios and the highest-risk aspects of the solution are the focus of the firstiterations in the elaboration phase. Note that the inception phase is not an iter-ation. You should not try to develop all—or even most—of the use case scenar-ios during inception. Usually about 10% of the use case scenarios are written inany detail during the inception phase.

Elaboration phase. During the elaboration phase, the system’s core architec-ture is iteratively developed, high-risk elements of the system are addressed,and most of the system requirements are defined. The core architecture iscomposed of those software components that are central to the system. Forexample, the objects represented in the class diagram in Figure 1-5 wouldmake up the core of that banking system.

The elaboration phase extends over two or more iterations. If the elabora-tion phase has only one iteration, no feedback can come into play. Multipleiterations are necessary when developing the correct solution incrementallyand adaptively.

If a solution is going to fail, it is usually because of high-risk elements thatcould not be resolved. Early in the elaboration phase, high-risk elements needto be addressed and resolved. If these elements are irresolvable, either the scopeof the project must be changed to remove them, or the project must be aban-doned. In either case, little time, money, and effort has been wasted. Leavingthe high-risk elements until later in the development of a project can lead totime and cost overruns—and poor time and cost estimates—because the resolu-tion of the high-risk issues will consume considerable resources. Moreover, ifthese issues cause the project to fail, much is lost. Address high-risk issues early.

Incep

tion

Oneiteration

Onephase

Elaboration Construction Transition

RUP development phases

FIGURE 1-7

The inception phase defines a project’s scope and feasibility

The elaboration phase developes a core system and addresses high-risk elements

The elaboration phase refines a sys-tem’s requirements and cost

Carrano_5e.book Page 21 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

22 Chapter 1 Principles of Programming and Software Engineering

Each iteration starts with an object-oriented analysis of the system require-ments and input from the testing of the previous iteration. This analysis leadsto a set of use case scenarios to analyze, design, implement, and test. Thosescenarios are examined for objects and attributes. Sequence and class diagramsare drawn to clearly show the objects’ collaborations, responsibilities to thesystem, and relationships to one another. This design is then coded and tested.If this iteration is not one of the first, the code may need to be integrated intothe existing subsystem. After final testing, end users and domain experts usethe system to produce feedback for subsequent iterations.

Construction phase. The construction phase begins once most of the systemrequirements have been formalized. Each iteration in this phase still consists ofanalysis, design, implementation, and testing. At this point in the develop-ment of the system, however, the analysis and design components require lessattention, so increased effort is focused on implementation and testing.

Transition phase. The transition phase begins as the system is put into theenvironment in which it was designed to work. This may involve beta testingwith advanced end users, or one of several other techniques to move thesystem into a production environment.

Figure 1-8 shows an example of the relative levels of work done duringeach of the phases in analysis, design, implementation, and testing. Thisexample simply suggests the work levels; do not take them literally.

The construction phase develops the remaining system

During the transition phase, beta testing and system devel-opment occur

Inception

AnalysisDesign

Elaboration

Phases

Iterations

1 ... ... ... ... n

Construction Transition

ImplementationTesting

Relative amounts of work done in each development phase

FIGURE 1-8

Carrano_5e.book Page 22 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Software Engineering and Object-Oriented Design 23

What About the Waterfall Method of Development?The RUP is probably new to you, as is another development process—thewaterfall method—in which the solution is developed by sequentially movingthrough phases such as requirements analysis, design, implementation, testing,and deployment. However, you doubtless will meet programmers during yourcareer who know only the waterfall method. For this reason, you should knowwhat it is, even though we think it is inferior to the RUP.

The waterfall method often results in the following actions:

Systems analysts produce a set of documents specifying the require-ments of the proposed system. The requirements documents arepassed to a design team that produces a set of design documents,which are passed to programmers who code the design. The code ispassed to a testing team, who verify that the code correctly imple-ments the requirements of the system, as specified by the systems ana-lysts at the beginning of the process.

Notice that the phases occur one after the other. If the system analysts donot understand everything about all aspects of the system and its environment,it is likely that they will specify the wrong system. Such aspects include the sys-tem’s end users and their needs, other computer systems that must be used,the global business environment, and so on. Incorrect requirements analysiswill create a design that, when implemented, leads to a correct system for thewrong problem. In other words, the system solves the problem defined by therequirements analysis, but the requirements analysis describes the wrongproblem.

The difficulty with trying to specify the requirements of the proposedsystem up front is that system requirements are not predictable. Most things inthe world today, especially in the high-tech and business worlds, are highlyspeculative. Change is constant. This implies that the requirements of anysystem might change at any time. The development process must allow thesystem under development to evolve and adapt to the changing environment.The waterfall method requires analysts to be omnipotent. For a system to suc-ceed, the waterfall method should not be used for its development. Iterative,incremental, evolutionary development processes, however, allow feedback toguide the adaptation of the system to a correct solution.

Be careful not to impose the waterfall method on the RUP developmentphases. The RUP’s inception phase is not only about specifying the require-ments of the system. Some, but not all, system requirements are specifiedduring this phase. During inception, programmers also make sure the system isfeasible, estimate how much the system will cost to develop, and identify thesignificant high-risk issues that might doom the project. The inception phase isnot the waterfall requirements phase. Trying to develop all use case scenariosduring inception makes it the same as the waterfall method.

The RUP’s elaboration phase is not only about designing the system.Some design, but not all, is done during this phase. During elaboration,

The waterfall method is outdated and should not be used

Carrano_5e.book Page 23 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

24 Chapter 1 Principles of Programming and Software Engineering

programmers also do analysis, coding, and testing. The elaboration phase isnot the waterfall design phase. Beware—if the elaboration phase degrades tohaving only one iteration, the development process effectively becomes thewaterfall method. Similarly, the RUP’s construction phase is not only aboutimplementation. The construction phase is not the waterfall implementationphase.

1.2 Achieving a Better Solution

The last program you wrote most likely solved the given problem correctly.However, was it the best possible solution? If you spent little time doing analy-sis and design, the solution probably left something to be desired. If you wereto code the same program again, you would doubtless produce a better solu-tion than the first attempt. However, if you spent some extra time analyzingthe problem and designing a solution, you would probably get your bestsolution.

Suppose that you generated three correct but different solutions. Can youidentify aspects of each solution that makes it better than the other solutions?What aspects of one solution make it better than another solution? Whataspects should you focus on to create better solutions? Let’s examine some ofthese aspects.

Evaluation of Designs and SolutionsCreating a good set of modules for a moderate size problem is more art thanscience. It requires experience on the part of the programmer. A givenproblem likely has no “best set of modules.” Some sets of modules—and theirinteractions—might be better than other sets of modules in light of certainmeasures. Moreover, for a sufficiently large problem, several different sets ofmodules could be considered “best” under any given measure.

The “better” designs, however, do adhere to certain principles, which weexamine next.

Cohesion. Each module should perform one well-defined task; that is, itshould be highly cohesive. A highly cohesive module brings several immediatebenefits to a design or solution.

First, the module, if well named, promotes self-documenting, easy-to-understand code. For example, a highly cohesive function called sort shoulddo nothing but sort. What this function does is clear from its name: If thisfunction also prints the sorted values, it is not cohesive.

Second, a highly cohesive module is easy to reuse in other softwareprojects. If a solution for another problem is being developed, the highly cohe-sive sort function can be used without change. If this function also prints thesorted values, it is much less likely to be the right sorting routine for the job.

Time devoted to analysis and design is time well spent

Highly cohesive modules perform one well-defined task

Carrano_5e.book Page 24 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Achieving a Better Solution 25

Third, a highly cohesive module is much easier to maintain. Because thehighly cohesive sort function does nothing but sort, fixing a logical error issimpler. If this function prints the sorted values, too, the printing code willcomplicate the function’s maintenance. A highly cohesive module has but onetask that might need revision.

Fourth, a highly cohesive module is more robust, that is, it is less likely tobe affected by change. The highly cohesive sort function will require changeonly if the system requires a different kind of sort. For example, you mightneed to sort data into descending, rather than ascending, order, or you mightneed a faster sort.

Finally, a highly cohesive module promotes low coupling, which wediscuss in the next section.

A guiding principle of OOD is that each class should have a single, well-defined responsibility. The methods of a class should be highly cohesive andrelated directly to supporting the responsibility of the class. The responsibilitiesof a class are functionally equivalent to the tasks that the class needs to per-form. If a class has too many responsibilities, it should be split into multipleclasses, each with a single responsibility taken from the original class.

Like many object-oriented principles, cohesion can be described in humanterms. A person with low cohesion has “too many irons in the fire.” Suchpeople tend to get bogged down in everything that they need to get done, andnothing they do gets done well. They should become more cohesive by dele-gating some of their responsibilities to others.

Coupling. The modules in a design should be independent of one another;that is, they should be loosely coupled. Two or more coupled modules aremodules that depend on each other. This could be in the form of anythingfrom sharing data structures to calling each other’s methods. Thus, somedegree of coupling is necessary to get work done. However, low coupling ben-efits a system in several ways.

First, a module with low coupling tends to create a system that is moreadaptable to change. If class A depends on (is coupled to) a class B and class Bis changed, it is very likely that these changes will affect class A and break it.

Second, a module with low coupling creates a system that is easier tounderstand. If class A depends on a class B, understanding how class A worksrequires an understanding of class B. Thus, class A is difficult to understand inisolation. A solution with a lot of coupling can become nearly impossible tounderstand.

Third, a module with low coupling increases the reusability of thatmodule. If class A depends on a class B, reusing class A in another program iscomplicated by also having to include class B in that program as well. Reusingcoupled modules requires reusing all of the coupled modules together as aunit. Often this is not desirable or possible.

Fourth, a module with low coupling has increased cohesion. As the cohe-sion associated with a module goes down, that module does more unrelatedwork, as we saw in the previous section. This has the side effect of causing the

A robust module performs well under unusual conditions

Loosely coupled modules are independent

Carrano_5e.book Page 25 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

26 Chapter 1 Principles of Programming and Software Engineering

module to be coupled with many other modules from other areas in theprogram.

Finally, realize that some coupling is required; coupling cannot and shouldnot be eliminated from designs and solutions. To get work done, objects mustcollaborate. But collaboration requires objects to depend on one another.Tasks that an object has delegated to other objects create coupling betweenthese objects. This coupling is necessary, but it should be kept to a minimum.

Class diagrams should show the dependencies among the classes in adesign. Two approaches to solving the same problem would result in two dif-ferent class diagrams. These diagrams would show different dependencies. Thediagram with the fewest dependencies represents the design with the lowestcoupling. This fact needs to be kept in perspective, however. As with every-thing, some designs with more coupling are better than other designs with lesscoupling because of other factors.

Minimal and complete interfaces. The interface for a class is made up of thepublicly accessible methods and data. Typically, a class interface contains onlymethods, as you will see, because publicly accessible data members generallycause problems. The interface for a class describes the only way for program-mers to interact with that class. Thus, interfaces are where collaborationbetween objects takes place. It is through its interface that a class is coupled toother classes. Designing good class interfaces is an important skill.

Each class should be easy to understand. Thus, when designing a class, youshould keep the number of its methods small. However, your classes shouldprovide programmers the power to do what they need to do easily. Thesedesires are at odds.

A complete interface for a class is one that will allow programmers toaccomplish any reasonable task, given the responsibilities of that class. Aminimal interface for a class is one that contains a method if and only if thatmethod is essential to that class’s responsibilities. Programmers can more easilylearn how to interact with a class that has a minimal interface, as it has fewermethods to understand. Classes with minimal interfaces are also much easier tomaintain. Changes to a class with an extensive interface could affect many of itsmethods.

You should evaluate classes in terms of how complete and minimal theirinterfaces are. It is very important that interfaces are complete. Of somewhatless importance is having a minimal interface; sometimes a nonessential methodis just too useful to omit.

The interface to a function or method is more accurately called its signa-ture. The signature consists of a function or method’s name; the number,order and types of the argument list; and any qualifiers such as const thatmight apply. A signature looks very much like the function or method’s proto-type, declaration, or header. You use the interface to a function or methodwhen you call it, sending it the correct number, order, and type of arguments.

Other important aspects of modules. As you design the modules for a solu-tion, you should think about how those modules relate to one another. As you

A module’s inter-face is the only way to interact with that module

Carrano_5e.book Page 26 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Achieving a Better Solution 27

have seen, keeping an eye on their cohesion and coupling can give you a wayof comparing designs and choosing the better one.

Also important is documenting each module’s responsibilities and itsrequirements for use. Additionally, you should make sure that each moduledoes what it is designed to do.

Operation ContractsAn operation contract documents how a method can be used and what limi-tations it has. You should begin specifying this contract during analysis, finishthe specification during design, and then document your code with this con-tract, particularly within the header files. In this way, programmers who useyour code will understand what contract they need to honor for the method togenerate correct results.

There are several things you need to specify for each method. A method’sinterface will specify how to call the method and the number, order, and typesof arguments it expects—as already discussed. You also need to specify whatshould be true before the method is called and what will be true after themethod finishes execution.

During design, it is also important that you clearly specify not only thepurpose of each module but also the data flow among modules. For example,you should provide answers to these questions for each module: What data isavailable to the module before its execution? What does the module assume?What actions have taken place, and what does the data look like after themodule executes? Thus, you should specify in detail the assumptions, input,and output for each module.

For example, if you as program designer needed to sort an array of inte-gers, you might write the following specifications for a sort function:

The function will receive an array of num integers, where num > 0.

The function will return the array with the integers sorted.

You can view these specifications as the terms of a contract between your func-tion and the module that calls it.

This contract helps programmers to understand what responsibilities thismodule will have to the other modules in the solution. Whoever writes the sortfunction must live up to this contract. After the sort function has been writtenand tested, the contract tells the rest of the program how to call the sort func-tion properly and the result of doing so.

Notice, however, that a module’s contract does not commit the module toa particular way of performing its task. If another part of the program assumesanything about the method, it does so at its own risk. Thus, for example, if atsome later date you rewrite your function to use a different sorting algorithm,you should not need to change the rest of the program at all. As long as thenew function honors the terms of the original contract, the rest of the programshould be oblivious to the change.

A module’s contract specifies the mod-ule’s purpose, assumptions, input, and output

Specify the data flow among modules

Specifications are the terms of a contract

A module’s opera-tion contract should not describe how it will perform its task

Carrano_5e.book Page 27 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

28 Chapter 1 Principles of Programming and Software Engineering

This should not be news to you. Although you might not have explicitlyused the term “contract” before, the concept should be familiar. You write acontract when you write a function’s precondition, which is a statement of theconditions that must exist at the beginning of a function, as well as when youwrite its postcondition, which is a statement of the conditions at the end of afunction. For example, the sort function that adheres to the previous contractcould appear in pseudocode1 as

sort(anArray, num)// Sorts an array.// Precondition: anArray is an array of num integers; num > 0.// Postcondition: The integers in anArray are sorted.

These particular pre- and postconditions actually are deficient, as can bethe case in a first-draft contract. For example, does “sorted” mean ascendingorder or descending order? How large can num be? While implementing thisfunction, you might assume that “sorted” means “ascending order” and thatnum will not exceed 100. Imagine the difficulties that can arise when anotherperson tries to use sort to sort an array of 500 integers into descending order.This user does not know your assumptions unless you documented them byrevising the contract as follows:

sort(anArray, num)// Sorts an array into ascending order.// Precondition: anArray is an array of num integers // and 1 <= num <= MAX_ARRAY, where MAX_ARRAY is a // global constant that specifies the maximum size // of anArray.// Postcondition: anArray[0] <= anArray[1] <= ... <= // anArray[num-1], num is unchanged.

When you write a precondition, begin by describing the method or func-tion’s input arguments, mention any global named constants that it uses, andfinally list any assumptions that it makes. When you write a postcondition,describe what changes the module has made. Note that in the case of amethod or function that returns a value—which is technically a part of thepostcondition—the value should be described.

Novice programmers tend to dismiss the importance of precise documen-tation, particularly when they are simultaneously designer, programmer, anduser of a small program. If you design sort but do not write down the termsof the contract, will you remember them when you later implement the func-tion? Will you remember how to use sort weeks after you have written it? Torefresh your memory, would you rather examine your C++ code or read a

1. Pseudocode in this book appears in italic.

Operation contracts should include precise pre-conditions and postconditions

First-draft specifications

Revised specifications

Precise documenta-tion is essential

Carrano_5e.book Page 28 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Achieving a Better Solution 29

simple set of pre- and postconditions? As the size of a program increases, gooddocumentation becomes even more important, regardless of whether you aresole author or part of a team.

You should not ignore the possibility that you or someone else has alreadyimplemented some of the required modules. C++ facilitates the reuse of soft-ware components, which are typically organized into libraries that have beencompiled. That is, you will not always have access to a class’s C++ code. C++provides a library that is an example of such a collection of preexisting software.For example, you know how to use the standard function sqrt contained in theC++ math library (cmath), yet you do not have access to its source statements,because it is precompiled. You know, however, that if you pass sqrt a floating-point expression, it will return the floating-point square root of the value of thatexpression. You can use sqrt even though you do not know its implementa-tion. Furthermore, it may be that sqrt was written in a language other thanC++! There is so much about sqrt that you do not know, yet you can use it inyour program without concern, as long as you know its specifications.

If, in the past, you have spent little or no time on analysis and design foryour programs, you must change this habit! The end result of the OODshould be a modular solution that is easy to translate into the constructs of aparticular programming language. By spending adequate time with analysisand design, you will spend less time writing and debugging your program.

VerificationFormal, theoretical techniques are available for proving that an algorithm iscorrect. Although research in this area is incomplete, it is useful to mentionsome aspects of the verification process.

An assertion is a statement about a particular condition at a certain pointin an algorithm. Preconditions and postconditions are simply assertions aboutconditions at the beginning and end of methods and functions. An invariant isa condition that is always true at a particular point in an algorithm. A loopinvariant is a condition that is true before and after each execution of an algo-rithm’s loop. As you will see, loop invariants can help you write correct loops.By using invariants, you can detect errors before you begin coding and therebyreduce your debugging and testing time. Overall, invariants can save you time.

Proving that an algorithm is correct is like proving a theorem in geometry.For example, to prove that a method or function is correct, you would startwith its preconditions—which are analogous to the axioms and assumptions ingeometry—and demonstrate that the steps of the algorithm lead to the post-conditions. To do so, you would consider each step in the algorithm and showthat an assertion before the step leads to a particular assertion after the step.

By proving the validity of individual statements, you can prove thatsequences of statements, and then methods and functions, classes, and finallythe program are correct. For example, suppose you show that if assertion A1 istrue and statement S1 executes, assertion A2 is true. Also, suppose you haveshown that assertion A2 and statement S2 lead to assertion A3. You can thenconclude that if assertion A1 is true, executing the sequence of statements S1

Incorporate existing software compo-nents into your design

You can prove the correctness of some algorithms

Carrano_5e.book Page 29 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

30 Chapter 1 Principles of Programming and Software Engineering

and S2 will lead to assertion A3. By continuing in this manner, you eventuallywill be able to show that the program is correct.

Clearly, if you discovered an error during the verification process, youwould correct your algorithm and possibly modify the analysis and/or design.Thus, by using invariants, it is likely that your algorithm will contain fewererrors before you begin coding. As a result, you will spend less time debuggingyour program.

You can formally prove that particular constructs such as if statements,loops, and assignments are correct. An important technique uses loop invari-ants to demonstrate the correctness of iterative algorithms. For example, wewill prove that the following simple loop computes the sum of the first n ele-ments in the array item:

// computes the sum of item[0], item[1], . . .,// item[n-1] for any n >= 1int sum = 0;int j = 0;while (j < n){ sum += item[j]; ++j;} // end while

Before this loop begins execution, sum is 0 and j is 0. After the loop executesonce, sum is item[0] and j is 1. In general,

sum is the sum of the elements item[0] through item[j-1]

This statement is the invariant for this loop.The invariant for a correct loop is true at the following points:

■ Initially, after any initialization steps, but before the loop begins execution

■ Before every iteration of the loop

■ After every iteration of the loop

■ After the loop terminates

For the previous loop example, these points are as follows:

int sum = 0;int j = 0; while (j < n){ sum += item[j]; ++j; } // end while

Loop invariant

The invariant is true here

The invariant is true here

The invariant is true here

The invariant is true here

Carrano_5e.book Page 30 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Achieving a Better Solution 31

You can use these observations to prove the correctness of an iterative(loop controlled) algorithm. For the previous example, you must show thateach of the following four points is true:

1. The invariant must be true initially, before the loop begins execu-tion for the first time. In the previous example, sum is 0 and j is 0 ini-tially. In this case, the invariant states that sum contains the sum of theelements item[0] through item[-1]; the invariant is true becausethere are no elements in this range.

2. An execution of the loop must preserve the invariant. That is, if theinvariant is true before any given iteration of the loop, you must showthat it is true after the iteration. In the example, the loop addsitem[j] to sum and then increments j by 1. Thus, after an executionof the loop, the most recent element added to sum is item[j-1]; thatis, the invariant is true after the iteration.

3. The invariant must capture the correctness of the algorithm. Thatis, you must show that if the invariant is true when the loop terminates,the algorithm is correct. When the loop in the previous example ter-minates, j contains n, and the invariant is true: sum contains the sumof the elements item[0] through item[n-1], which is the sum thatyou intended to compute.

4. The loop must terminate. That is, you must show that the loop willterminate after a finite number of iterations. In the example, j beginsat 0 and then increases by 1 at each execution of the loop. Thus, jeventually will equal n for any n ≥ 1. This fact and the nature of thewhile statement guarantee that the loop will terminate.

Not only can you use invariants to show that your loop is correct, but youcan also use them to show that your loop is wrong. For example, suppose thatthe expression in the previous while statement was j <= n instead of j < n.Steps 1 and 2 of the previous demonstration would be the same, but Step 3would differ: When the loop terminated, j would contain n + 1 and, becausethe invariant would be true, sum would contain the sum of the elementsitem[0] through item[n]. Because this is not the desired sum, you knowthat something is wrong with your loop.

Notice the clear connection between Steps 1 through 4 and mathemati-cal induction.2 Showing the invariant to be true initially, which establishes thebase case, is analogous to establishing that a property of the natural numbersis true for 0. Showing that each iteration of the loop preserves the invariant isthe inductive step. This step is analogous to showing that if a property is truefor an arbitrary natural number k, then the property is true for the naturalnumber k + 1. After performing the four steps just described, you canconclude that the invariant is true after every iteration of the loop—just as

2. A review of mathematical induction appears in Appendix D.

Steps to establish the correctness of an algorithm

Carrano_5e.book Page 31 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

32 Chapter 1 Principles of Programming and Software Engineering

mathematical induction allows you to conclude that a property is true for everynatural number.

Identifying loop invariants will help you write correct loops. You shouldstate the invariant as a comment that either precedes or begins each loop,as appropriate. For example, in the previous example, you might write thefollowing:

// Invariant: 0 <= j <= n and // sum = item[0] + ... + item[j-1]while (j < n) . . .

You should confirm that the invariants for the following unrelated loopsare correct. Remember that each invariant must be true both before the loopbegins and after each iteration of the loop, including the final one. Also, youmight find it easier to understand the invariant for a for loop if you tempo-rarily convert it to an equivalent while loop:

// Computes n! for an integer n >= 0 int f = 1;// Invariant: f == (j-1)!for (int j = 1; j <= n; ++j) f *= j;

// Computes an approximation to ex for a real xdouble t = 1.0;double s = 1.0;int k = 1; // Invariant: t == xk-1/(k-1)! and // s == 1+x+x2/2!+...+xk-1/(k-1)!while (k <= n){ t *= x/k; s += t; ++k;} // end while

What Is a Good Solution?Before you devote your time and energy to the study of problem-solving tech-niques, it seems only fair that you see at the outset why mastery of these tech-niques will help make you a good problem solver. An obvious statement is thatthe use of these techniques will produce good solutions. This statement, how-ever, leads to the more fundamental question: What is a good solution? A briefattempt at answering this question concludes this section.

Because a computer program is the final form your solutions will take,consider what constitutes a good computer program. Presumably, you write a

State loop invariants in your programs

Examples of loop invariants

Carrano_5e.book Page 32 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Achieving a Better Solution 33

program to perform some task. In the course of performing that task, there is areal and tangible cost of a program. This cost includes such factors as thecomputer resources (computing time and memory) that the program con-sumes, the difficulties encountered by those who use the program, and theconsequences of a program that does not behave correctly.

However, the costs just mentioned do not give the whole picture. Theypertain to only the life of the solution after it has been developed. In assessingwhether a solution is good, you also must consider the effort required todevelop the solution and any changes (bug fixes or extensions) to the programthat are made after the program has been deployed. Each of these incurs costs,too. The total cost of a solution must take into account the value of the timeof the people who analyzed, designed, coded, debugged, and tested it. A solu-tion’s cost must also include the cost of maintaining, modifying, and expand-ing it.

Thus, when calculating the overall cost of a solution, you must include adiverse set of factors. If you adopt such a multidimensional view of cost, it isreasonable to evaluate a solution against the following criterion:

A solution is good if the total cost it incurs over all phases of its life cycle isminimal.

It is interesting to consider how the relative importance of the various compo-nents of this cost has changed since the early days of computing. In the begin-ning, the cost of computer time relative to human time was extremely high. Inaddition, people tended to write programs to perform very specific, narrowlydefined tasks. If the task changed somewhat, a new program was written.Program maintenance was probably not much of an issue, so there was littleconcern if a program was hard to read. A program typically had only oneuser—its author. As a consequence, programmers tended not to worry aboutmisuse or ease of use of their programs; a program’s interface generally was notconsidered important.

In this type of environment, one cost clearly overshadowed all others:computer resources. If two programs performed the same task, the one thatrequired less time and memory was better. How things have changed! Sincethe early days of computers, computing costs have dropped dramatically, thusmaking the value of the problem solver’s and programmer’s time a much moresignificant factor in the cost of a solution. Another consequence of the drop incomputing costs is that computers now are used to perform tasks in a widevariety of areas, many of them nonscientific. People who interact with comput-ers often have no technical expertise and no knowledge of the workings of pro-grams. People want their software to be easy to use.

Today, programs are larger and more complex than ever before. They areoften so large that many people are involved in their development. Goodstructure and documentation are thus of the utmost importance. As programsperform more highly critical tasks, the costs associated with malfunctions willsoar. Thus, society needs both well-structured programs and techniques forformally verifying their correctness. People will not and should not entrust

A multidimensional view of a solution’s cost

Programs must be well structured and documented

Carrano_5e.book Page 33 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

34 Chapter 1 Principles of Programming and Software Engineering

their livelihoods—or their lives—to a program that only its authors can under-stand and maintain.

These developments have made obsolete the notion that the most effi-cient solution is always the best. If two programs perform the same task, it isno longer true that the faster one is necessarily better. Programmers who useevery trick in the book to save a few microseconds of computing time at theexpense of clarity are not in tune with the cost structure of today’s world. Youmust write programs with people as well as computers in mind.

At the same time, do not get the impression that the efficiency of a solu-tion is no longer important. To the contrary, many situations occur for whichefficiency is the prime determinant of whether a solution is even usable. Thepoint is that a solution’s efficiency is only one of many factors that you mustconsider. If two solutions have approximately the same efficiency, other factorsshould dominate the comparison. However, when the efficiencies of solutionsdiffer significantly, this difference can be the overriding concern. The stages ofthe problem-solving process at which you should be most concerned aboutefficiency are those during which you develop the underlying algorithm. Thechoice of a solution’s components—the objects—and the design of the inter-actions between those objects—rather than the code you write, leads to signifi-cant differences in efficiency.

This book advocates a problem-solving philosophy that views the cost of asolution as multidimensional. This philosophy is reasonable in today’s world,and it likely will be reasonable in the years to come.

1.3 Key Issues in Programming

Given that a good solution is one that, in the course of its life cycle, incurs asmall cost, the next questions to ask are: What are the specific characteristics ofgood solutions? How can you construct good solutions? This section providessome answers to these very difficult questions.

The programming issues that this section discusses should be familiar toyou. However, the novice programmer usually does not truly appreciate theirimportance. After the first course in programming, many students still simplywant to “get the thing to run.” The discussion that follows should help yourealize just how important these issues really are.

One of the most widespread misconceptions held by novice programmersis that a computer program is “read” only by a computer. As a consequence,they tend to consider only whether the computer will be able to “understand”the program—that is, will the program compile, execute, and produce thecorrect output? The truth is, of course, that other people often must read andmodify programs. In a typical programming environment, many individualsshare a program. One person may write a program, which other people use inconjunction with other programs written by other people, and a year later, adifferent person may modify the program. It is therefore essential that you takegreat care to design a program that is easy to read and understand.

Efficiency is only one aspect of a solution’s cost

People read pro-grams, too

Carrano_5e.book Page 34 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 35

You should always keep in mind the following seven issues of programming:

ModularityAs this book will continually emphasize, you should strive for modularity in allphases of the problem-solving process, beginning with the initial design of asolution. You know from the earlier discussion of modular design that manyprogramming tasks become more difficult as the size and complexity of aprogram grows. Modularity slows the rate at which the level of difficultygrows. More specifically, modularity has a favorable impact on the followingaspects of programming:

■ Constructing the program. The primary difference between a smallmodular program and a large modular program is simply the number ofmodules each contains. Because the modules are independent, writing onelarge modular program is not very different from writing many small,independent programs, although the interrelationships among modulesmake the design much more complicated. On the other hand, working ona large nonmodular program is more like working on many interrelatedprograms simultaneously. Modularity also permits team programming,where several programmers work independently on their own modulesbefore combining them into one program.

■ Debugging the program. Debugging a large program can be a mon-strous task. Imagine that you type a 10,000-line program and eventuallyget it to compile. Neither of these tasks would be much fun. Now imaginethat you execute your program and, after a few hundred lines of output,you notice an incorrect number. You should anticipate spending the nextday or so tracing through the intricacies of your program before discover-ing a problem such as an array index that is too large.

A great advantage of modularity is that the task of debugging a largeprogram is reduced to one of debugging many small programs. When youbegin to code a module, you should be almost certain that all othermodules coded so far are correct. That is, before you consider a module

Seven Key Programming Issues1. Modularity2. Style3. Modifiability4. Ease of use5. Fail-safe programming6. Debugging7. Testing

KEY CONCEPTS

Modularity facili-tates programming

Modularity isolates errors

Carrano_5e.book Page 35 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

36 Chapter 1 Principles of Programming and Software Engineering

finished, you should test it extensively, both separately and in context withthe other modules, by calling it with actual arguments carefully chosen toinduce all possible behaviors of the modules. If this testing is done thor-oughly, you can feel fairly sure that any problem is a result of an error inthe last module added. Modularity isolates errors.

More theoretically, as was mentioned before, you can use formal tech-niques to establish the correctness of a program. Modular programs areamenable to this verification process.

■ Reading the program. A person reading a large program may havetrouble seeing the forest for the trees. Just as a modular design helps theprogrammer cope with the complexities of solving a problem, so too will amodular program help its reader understand how the program works. Amodular program is easy to follow because the reader can get a good ideaof what is going on without reading any of the code. A well-written func-tion can be understood fairly well from only its name, initial comments,and the names of the other functions that it calls. Readers of a programneed to study actual code only if they require a detailed understanding ofhow the program operates. Program readability is discussed further in thenext section on style.

■ Modifying the program. Modifiability will be discussed in greater detaillater in this chapter, but as the modularity of a program has a directbearing on its modifiability, a brief mention is appropriate here. A smallchange in the requirements of a program should require only a smallchange in the code. If this is not the case, it is likely that the program ispoorly written and, in particular, that it is not modular. To accommodate asmall change in the requirements, a modular program usually requires achange in only a few of its modules, particularly when the modules areindependent (that is, loosely coupled) and each module performs a singlewell-defined task (that is, is highly cohesive). This is of particular impor-tance with iterative development techniques, which produce changes inprogram requirements at every iteration.

When making changes to a program, it is best to make a few at a time.By maintaining a modular design, you can reduce a fairly large modifica-tion to a set of small and relatively simple modifications to isolated parts ofthe program. Modularity isolates modifications.

■ Eliminating redundant code. Another advantage of modular design isthat you can identify a computation that occurs in many different parts ofthe program and implement it as a function. Thus, the code for thecomputation will appear only once, resulting in an increase in both read-ability and modifiability.

StyleThis section considers the following seven issues of personal style inprogramming:

Modular programs are easy to read

Modularity isolates modifications

Modularity elimi-nates redundancies

Carrano_5e.book Page 36 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 37

Admittedly, much of the following discussion reflects the personal taste of theauthor; certainly other good programming styles are possible.

Use of private data members. Each object has a set of methods that repre-sents the operations that can be performed on the object. The object also con-tains data members for storing information within the object. You should hidethe exact representation of these data members from modules that use theobject by making all of the data members private. Doing so supports the prin-ciple of information hiding. The details of the object’s implementation arehidden from view, with methods providing the only mechanism for gettinginformation to and from the object. When the only operations involved with aparticular data member are retrieve and modify, the object should provide asimple method—called an accessor—that returns the value of the data memberand another method—called a mutator—that sets the value of the datamember. For example, a Person object could provide access to the datamember theName through the methods getName to return the person’s nameand setName to change the person’s name.

Proper use of reference arguments. A method does interact, in a controlledfashion, with the rest of the program via its arguments. Value arguments,which are the default when you do not write & after the formal argument’sdata type, pass values into the method. Pass-by-value actual arguments arecopied into their corresponding formal arguments, which are local to themethod. Thus, any change that the method makes to these formal argumentsis not reflected in the actual arguments back in the calling program. The com-munication between the calling program and the method is one-way. Becausethe restriction to one-way communication supports the notion of an isolatedmodule, you should use value arguments when possible. However, passinglarge objects by value is not very efficient because of the copying required.

Value arguments are represented in UML class diagrams by the in direc-tion specifier in the parameter list to a method. Figure 1-9 shows a possiblevalue argument arg1.

Seven Issues of Style1. Use of private data members2. Proper use of reference arguments3. Proper use of methods4. Avoidance of global variables in modules5. Error handling6. Readability7. Documentation

KEY CONCEPTS

Data members should always be private

Value arguments pass values into a method

Reference argu-ments return values from a method

Carrano_5e.book Page 37 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

38 Chapter 1 Principles of Programming and Software Engineering

How and when is it appropriate to use reference arguments (also calledpass-by-reference)? Recall that a reference formal argument becomes an alias(another name) for the actual argument. With pass-by-reference, the actualargument is not copied into the formal argument. Thus, changes to the formalargument within the method also occur in the actual argument in the callingmodule. The obvious situation requiring reference arguments is when amethod needs to return several values to the calling module.

Reference arguments are represented in UML class diagrams by the out orinout direction specifier in the parameter list to a method. Figure 1-9 showstwo reference arguments, arg2 and arg3. Whenever a value must be passed outof a method through an argument it must be a reference argument.

Suppose that you are writing a method that requires access to a largeobject x, whose value should not be altered. You consider passing x as a valueargument; however, to increase the efficiency of your program, you would liketo avoid the computer-time and storage requirements of making a copy of x.So, what about passing x as a reference argument? The problem with making xa reference argument is that it conveys misinformation about the method’srelation to the rest of the program. By convention, a reference argument isused to communicate a value from the method back to its calling module. Ref-erence arguments whose values remain unchanged, however, make theprogram more difficult to read and more prone to errors if modifications arerequired. The situation is analogous to using a variable whose value neverchanges when you really should use a constant. The solution is to precede theformal argument with const, which prevents the method from changing thecorresponding actual argument. Thus, we have the efficiency of pass-by-reference (no copy is made) with the protection of pass-by-value (the argu-ment is input only). This is called pass-by-constant-reference.

Constant reference arguments are represented in UML class diagrams byan in direction specifier in the parameter list to a method, just like value argu-ments. Whether you implement arg1 in Figure 1-9 as a value argument or aconstant reference argument depends on the size of its type. If objects ofSomeType are large and complex, then use pass-by-constant-reference; other-wise pass-by-value will work fine.

Proper use of methods. To reduce coupling among modules, you should restrictthe calls that a method can make. A method should call only other methods:

Class diagram showing various parameter semantics

FIGURE 1-9

SomeClass

+input(in arg1:SomeType)+output1(out arg2:SomeType)+output2(inout arg3:SomeType)

When copying the argument is expen-sive, use pass-by-constant-reference instead of pass-by-value

To reduce coupling, restrict what a method can do

Carrano_5e.book Page 38 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 39

■ Defined within the same class

■ Of argument objects

■ Of objects created within the method

■ Of objects contained within the same class as data members

For example, suppose we have the following code:

class Building{public: void turnOnLight() { appliance.getLamp().turnOn(); }private: Appliance appliance;}; // end Building

class Appliance{public: Lamp getLamp() { return light; }private: Lamp light;}; // end Appliance

class Lamp{public: void turnOn();}; // end Lamp

Here, the turnOnLight method fails to follow these rules. It calls the methodturnOn from the Lamp class, although a Lamp object is not part of theBuilding class and has not been passed to the turnOnLight method as anargument. This code has coupled the Building class to the Lamp class.

A much better design would decouple the Building and Lamp classes, asfollows:

class Building{public: void turnOnLight() { appliance.turnOnLamp(); }private: Appliance appliance;}; // end Building

Building is coupled to Lamp

Building and Lamp are not coupled

Carrano_5e.book Page 39 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

40 Chapter 1 Principles of Programming and Software Engineering

class Appliance{public: void turnOnLamp() { light.turnOn(); }private: Lamp light;}; // end Appliance

class Lamp{public: void turnOn();}; // end Lamp

Now, the building asks the appliance to turn on the lamp, and the applianceturns on its light.

Suppose that the lamp’s switch is replaced with a dimmer-type switch. Wewould need to change the interface of the turnOn method in the Lamp class toturnOn(in howMuch:float). Finding the call to turnOn in the original codesegment might be difficult, because the Building class does not have explicitaccess to a Lamp object. This type of dependency makes a solution more diffi-cult to modify.

In the second code segment, the Appliance class has a Lamp object as adata member. You can easily find the calls to this object if necessary.

Avoidance of global variables in modules. One of the main advantages ofthe concepts of encapsulation and information hiding is the creaton of isolatedmodules. This isolation is sacrificed when a module accesses a global variable,because the effects of a module’s action are no longer self-contained or limitedto output arguments. That is, such a module has a side effect. Hence, the iso-lation of both errors and modifications is greatly compromised when globalvariables appear in modules.

Error handling. A program should check for errors in both its input and itslogic and attempt to behave gracefully when it encounters them. A methodshould check for certain types of errors, such as invalid input or argument values.What action should a method take when it encounters an error? Depending oncontext, the appropriate action in the face of an error can range from ignoringerroneous data and continuing execution to terminating the program. Acommon technique is for a method to return a boolean value to the callingmodule to indicate that it had encountered an error. Thus, the method leaveserror handling to the calling module. In general, methods should either return avalue or throw an exception instead of displaying a message when an error occurs.

Readability. For a program to be easy to follow, it should have a good struc-ture and design, a good choice of identifiers, good indentation and use of

Do not use global variables

In case of an error, methods should return a value or throw an exception, but not display a message

Carrano_5e.book Page 40 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 41

blank lines, and good documentation. You should avoid clever programmingtricks that save a little computer time at the expense of much human time. Youwill see examples of these points in programs throughout the book.

Choose identifiers that describe their purpose, that is, are self-docu-menting. Distinguish between keywords, such as int, and user-defined identi-fiers. This book uses the following conventions:

■ Keywords are lowercase and appear in boldface.

■ Names of standard functions are lowercase.

■ User-defined identifiers use both upper- and lowercase letters, as follows:

� Class names are nouns, with each word in the identifier capitalized.

� Method names are verbs, with the first letter lowercase and subsequent internal words capitalized.

� Variables begin with a lowercase letter, with subsequent words in the identifier capitalized.

� Data types declared in a typedef statement and names of structures and enumerations each begin with an uppercase letter.

� Named constants and enumerators are entirely uppercase and use underscores to separate words.

■ Two other naming conventions are followed as a learning aid:

� Data types declared in a typedef statement end in Type.

� Exception names end in Exception.

Use a good indentation style to enhance the readability of a program. Thelayout of a program should make it easy for a reader to identify the program’smodules. Use blank lines to offset each method. Also, within both methodsand the main program, you should offset with blank lines and indent individ-ual blocks of code visibly. These blocks are generally—but are not limited to—the actions performed within a control structure, such as a while loop or anif statement.

You can choose from among several good indentation styles. The fourmost important general requirements of an indentation style are:

■ Blocks should be indented sufficiently so that they stand out clearly.

■ Indentation should be consistent: Always indent the same type of con-struct in the same manner.

■ The indentation style should provide a reasonable way to handle theproblem of rightward drift, the problem of nested blocks bumpingagainst the right-hand margin of the page.

■ In a compound statement, the open and closed braces should line up:

Identifier style

Two learning aids

Guidelines for inden-tation style

Carrano_5e.book Page 41 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

42 Chapter 1 Principles of Programming and Software Engineering

{ <statement1> <statement2> . . . <statementn>}

Although it is preferable to place the open brace on its own line, spacerestrictions in this book prevent doing so except at the beginning of amethod or function’s body.

■ In order to prevent future errors, you should always make the body of allcontrol structures a compound statement, even if it consists of only oneline of code. If the open and close braces are included around single-statement control structure bodies, those control structures are preparedfor multiple statements. A common programmer error is to include asecond statement in the single-statement “body” of a control structure,thinking that it will execute within the body of that control structure.Because of space restrictions, this book will not follow this style.

Within these guidelines is room for personal taste. Here is a summary of thestyle you will see in this book:

■ A for or while statement is written for a simple action as

while (expression) statement

and for a compound action as

while (expression){ statements} // end while

■ A do statement is written for a simple action as

do statementwhile (expression);

and for a compound action as

do { statements} while (expression);

Indentation style in this book

Carrano_5e.book Page 42 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 43

■ An if statement is written for simple actions as

if (expression) statement1else statement2

and for compound actions as

if (expression){ statements}

else{ statements} // end if

One special use of the if statement warrants another style. Nested ifstatements that choose among three or more different courses of action,such as

if (condition1) action1else if (condition2) action2 else if (condition3) action3

are written as

if (condition1) action1else if (condition2) action2else if (condition3) action3

This indentation style better reflects the nature of the construct, which islike a generalized switch statement:

case condition1 : action1; break;case condition2 : action2; break;case condition3 : action3; break;

Carrano_5e.book Page 43 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

44 Chapter 1 Principles of Programming and Software Engineering

■ Braces are used to increase readability, even when they are not a syntacticnecessity. For example, in the construct

while (expression){ if (condition1) statement1 else statement2} // end while

the braces are syntactically unnecessary, because an if is a single state-ment. However, the braces highlight the scope of the while loop.

Documentation. A program should be well documented so that others canread, use, and modify it easily. Many acceptable styles for documentation are inuse today, and exactly what you should include often depends on the particularprogram or your individual circumstances.

This book will use a special form of documentation comment, called aJavadoc-style comment because it originated with the Java™ programminglanguage. These comments start with /** and end with */ and contain tagsthat classify different parts of your documentation. Several utility programs areavailable to read these comments and generate HTML-based documentation.One of these programs is called doxygen. All of the source code in the bookhas been read by doxygen, and the Web-based documentation that it gener-ated is available from the book’s Web site.

The advantage of using Javadoc-style comments—and a documentationsystem, like doxygen—is that you can edit the documentation for your sourcecode as you make changes to that code. The documentation lives directly inthe source code—as a Javadoc-style comment—making it very easy and conve-nient to keep your documentation up to date.

The following are the essential features of any program’s documentation—with associated commenting tags in parentheses:

Highly formatted, HTML-based docu-mentation is easy to produce

Essential Features of Program Documentation1. An initial comment at the top of each source code file that includes:

a. File name (@file)b. Statement of purposec. Author (@author)d. Date (@date)e. Optional file version number (@version)

(continues)

KEY CONCEPTS

Carrano_5e.book Page 44 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 45

Beginning programmers tend to downplay the importance of documenta-tion because comments do not affect a program’s logic. By now, you shouldrealize that people also read programs. Your comments must be clear enoughfor someone else to either use your module in a program or modify it. Thus,some of your comments are for people who want to use your module, whileothers are for people who will revise its implementation. You should be con-scious of different kinds of comments.

Beginners have a tendency to document programs as a last step. Youshould, however, write documentation as you develop the program. Becausethe task of writing a large program might extend over a period of severalweeks, you may find that the module that seemed so obvious when you wroteit last week will seem confusing when you try to revise it next week. Why notbenefit from your own documentation by writing it now rather than later?

See Appendix F for more details on the use of the Javadoc-style com-ments and the doxygen documentation system.

ModifiabilityAfter each iteration during the development of a program, the design willchange to some degree. This requires that your program be written in a waythat makes it easy to modify. This section offers two examples of how you canmake a program easy to modify: named constants and typedef statements.

Named constants. The use of named constants is a way to enhance the modi-fiability of a program. For example, the restriction that an array must be of apredefined, fixed size causes a bit of difficulty. Suppose that a program uses anarray to process the SAT scores of the computer science majors at your

2. Initial comments for each class that includes:a. Name of class and its header file (@class)b. Statement of purpose

3. Initial comments for each method or function that includes:a. Statement of purposeb. Description of each argument in the argument list (@param)c. Preconditions (@pre)d. Postconditions (@post)e. Exceptions thrown (@throw)f. Return value (@return)

4. Standard C++ comments in the body of each method or function to explain important features or subtle logic

Consider who will read your com-ments when you write them

You benefit from your own documen-tation by writing it now instead of later

Named constants make a program easier to modify

Carrano_5e.book Page 45 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

46 Chapter 1 Principles of Programming and Software Engineering

university. When the program was written, there were 202 computer sciencemajors, so the array was declared by

int scores[202];

The program processes the array in several ways. For example, it reads thescores, writes the scores, and averages the scores. The pseudocode for each ofthese tasks contains a construct such as

for (index = 0 through 201) Process the score

If the number of majors should change, not only do you need to revise the dec-laration of scores, but you also must change each loop that processes the arrayto reflect the new array size. In addition, other statements in the program mightdepend on the size of the array. A 202 here, a 201 there—which to change?

On the other hand, if you use a named constant such as

const int NUMBER_OF_MAJORS = 202;

you can declare the array by using

int scores[NUMBER_OF_MAJORS];

and write the pseudocode for the processing loops in this form:

for (index = 0 through NUMBER_OF_MAJORS - 1) Process the score

If you write expressions that depend on the size of the array in terms of theconstant NUMBER_OF_MAJORS (such as NUMBER_OF_MAJORS - 1), you canchange the array size simply by changing the definition of the constant andcompiling the program again.

The typedef statement. Suppose that your program performs floating-pointcomputations of type float, but you discover that you need greater precisionthan float variables provide. To change the relevant float declarations tolong double, for example, you would have to locate all such declarations anddecide whether to make the change.

You can simplify this change by using a typedef statement, which givesanother name to an existing data type. For example, the statement

typedef float RealType;

declares RealType as a synonym for float and allows you to use RealTypeand float interchangeably. If you declare all the relevant items in the previous

typedef state-ments make a program easier to modify

Carrano_5e.book Page 46 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 47

program as RealType instead of float, you can make your program easier tomodify and to read. To revise the precision of the computations, you wouldsimply change the typedef statement to

typedef long double RealType;

Ease of UseAnother area in which you need to keep people in mind is the design of theuser interface. Humans often process a program’s input and output. Here are afew obvious points:

■ In an interactive environment, the program should always prompt the userfor input in a manner that makes it quite clear what it expects. For exam-ple, the prompt “?” is not nearly as enlightening as the prompt “Pleaseenter your account number.” You should never assume that the users ofyour program will know what response the program requires.

■ A program should always echo its input. Whenever a program reads data,either from a user or from a file, the program should include the values itreads in its output. This inclusion serves two purposes: First, it gives theuser a check on the data entered—a guard against typos and errors in datatransmission. This check is particularly useful in the case of interactiveinput. Second, the output is more meaningful and self-explanatory when itcontains a record of what input generated the output.

■ The output should be well labeled and easy to read. An output of

1800 6 1Jones, Q. 223 2234.00 1088.19 N, J Smith, T. 111110.23 I, Harris, V. 44 44000.00 22222.22

is more prone to misinterpretation than

CUSTOMER ACCOUNTS AS OF 1800 HOURS ON JUNE 1

Account status codes: N=new, J=joint, I=inactive

NAME ACC# CHECKING SAVINGS STATUS

Jones, Q. 223 $ 2234.00 $ 1088.19 N, JSmith, T. 111 $ 110.23 --------- IHarris, V. 44 $44000.00 $22222.22 ------

These characteristics of a good user interface are only the basics. Severalmore subtle points separate a program that is merely usable from one that isuser friendly. Students tend to ignore a good user interface, but by investing alittle extra time here, you can make a big difference: the difference between agood program and one that only solves the problem. For example, consider aprogram that requires a user to enter a line of data in some fixed format, with

Prompt the user for input

Echo the input

Label the output

A good user inter-face is important

Carrano_5e.book Page 47 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

48 Chapter 1 Principles of Programming and Software Engineering

exactly one blank between the items. A free-form input that allows any numberof blanks between the items would be much more convenient for the user. Ittakes so little time to add a loop that skips blanks, so why require the user tofollow an exact format? Once you have made this small additional effort, it is apermanent part of both your program and your library of techniques. The userof your program never has to think about input format.

Fail-Safe ProgrammingA fail-safe program is one that will perform reasonably no matter how anyoneuses it. Unfortunately, this goal is usually unattainable. A more realistic goal isto anticipate the ways that people might misuse the program and to guardcarefully against these abuses.

This discussion considers two types of errors. The first type is an error ininput data. For example, suppose that a program expects a nonnegativeinteger but reads –12. When a program encounters this type of problem, itshould not produce incorrect results or abort with a vague error message.Instead, a fail-safe program provides a message such as

-12 is not a valid number of children.Please enter this number again.

The second type of error is an error in the program logic. Although a dis-cussion of this type of error belongs in the next section about debugging,detecting errors in program logic is also an issue of fail-safe programming. Aprogram that appears to have been running correctly may at some pointbehave unexpectedly, even if the data that it reads are valid. For example, theprogram may not have accounted for the particular data that elicited the sur-prise behavior, even though you tried your best to test the program’s logic. Orperhaps you modified the program and that modification invalidated anassumption that you made in some other part of the program. Whatever thedifficulty, a program should have built-in safeguards against these kinds oferrors. It should monitor itself and be able to indicate that something is wrongand you should not trust the results.

Guarding against errors in input data. Suppose that you are computing sta-tistics about the people in income brackets between $10,000 and $100,000.The brackets are rounded to the nearest thousand dollars: $10,000, $11,000,and so on to $100,000. The raw data is a file of one or more lines of the form

G N

where N is the number of people with an income that falls into the G-thou-sand-dollar group. If several people have compiled the data, several entries forthe same value of G might occur. As the user enters data, the program mustadd up and record the number of people for each value of G. From the prob-lem’s context, it is clear that G is an integer in the range 10 to 100 inclusive,and N is a nonnegative integer.

Check for errors in input

Check for errors in logic

Carrano_5e.book Page 48 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 49

As an example of how to guard against errors in input, consider an inputfunction for this problem. The first attempt at writing this function will illus-trate several common ways in which a program can fall short of the fail-safeideal. Eventually you will see an input function that is much closer to the fail-safe ideal than the original solution.

A first attempt at the function might be

const int LOW_END = 10; // Ten-thousand dollar income.const int HIGH_END = 100; // 100-thousand dollar income.const int TABLE_SIZE = HIGH_END - LOW_END + 1;

typedef int TableType[TABLE_SIZE];

int index(int group){ return group - LOW_END;} // end index

/** Reads and organizes income statistics. * @param incomeData A TableType of income statistics. * @pre The calling module gives directions and prompts * user. Input data is error-free and each input line is * in the form G N, where N is the number of people with * an income in the G-thousand-dollar group and LOW_END * <= G <= HIGH_END. An input line with values of zero * for both G and N terminates the input. * @post incomeData[G - LOW_END] = total number of people with * an income in the G-thousand-dollar group for each G * read. The values read are displayed. */void readData(TableType incomeData){ int group, number; // input values

// clear array for (group = LOW_END; group <= HIGH_END; ++group) incomeData[index(group)] = 0;

for (cin >> group >> number; (group != 0) || (number != 0); cin >> group >> number) { // Invariant: group and number are not both 0 cout << "Income group " << group << " contains " << number << " people.\n"; incomeData[index(group)] += number; } // end for} // end readData

/** @return The array index that corresponds to group number. */

This function is not fail-safe

Carrano_5e.book Page 49 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

50 Chapter 1 Principles of Programming and Software Engineering

This function has some problems. If an input line contains unexpected data,the program will not behave reasonably. Consider two specific possibilities:

■ The first integer on the input line, which the function assigns togroup, is not in the range LOW_END to HIGH_END. The reference

incomeData[index(group)]

will then be incorrect.

■ The second number on the input line, which the function assigns to number,is negative. Although a negative value for number is invalid because youcannot have a negative number of people in an income group, the functionwill add number to the group’s array entry. Thus, the array incomeData willbe incorrect.

After the function reads values for group and number, it must check to seewhether group is in the range LOW_END to HIGH_END and whether number ispositive. If either value is not in that range, you must handle the input error.

Instead of checking number, you might think to check the value ofincomeData[index(group)], after adding number, to see whether it is posi-tive. This approach is insufficient, however. First, notice that it is possible toadd a negative value to an entry of incomeData without that entry becomingnegative. For example, if number is – 4,000 and the corresponding entry inincomeData is 10,000, the sum is 6,000. Thus, a negative value for numbercould remain undetected and invalidate the results of the rest of the program.

One possible course of action for the function to take when it detectsinvalid data is to set an error flag and terminate. Another possibility is for it toset an error flag, ignore the bad input line, and continue. Which action iscorrect really depends on how the program uses the data once it is read.

The following readData function attempts to be as universally applicableas possible and to make the program that uses it as modifiable as possible. Whenthe function encounters an error in input, it sets a flag, ignores the data line,and continues. By setting a flag, the function leaves it to the calling module todetermine the appropriate action—such as abort or continue—when an inputerror occurs. Thus, you can use the same input function in many contexts andcan easily modify the action taken upon encountering an error.

/** Reads and organizes income statistics. * @param incomeData A TableType of income statistics. * @pre The calling program gives directions and prompts the * user. Each input line contains exactly two integers in * the form G N, where N is the number of people with an * income in the G-thousand-dollar group and LOW_END <= * G <= HIGH_END. An input line with values of zero for * both G and N terminates the input. * @post incomeData[G - LOW_END] = total number of people with * an income in the G-thousand-dollar group. The values * read are displayed. If either G or N is erroneous

Test for invalid input data

A function that includes fail-safe programming

Carrano_5e.book Page 50 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 51

* (either G < LOW_END, G > HIGH_END, or N < 0), the * function ignores the data line and continues. * @return false if either G or N are erroneous * (either G < LOW_END, G > HIGH_END, or N < 0) for * any data line read. In this case, the calling * program should take action. The return value is * true if the data is error-free. */bool readData(TableType incomeData){ int group, number; // input values bool dataCorrect = true; // no data error found as yet

for (group = LOW_END; group <= HIGH_END; ++group) incomeData[index(group)] = 0;

for (cin >> group >> number; (group != 0) || (number != 0); cin >> group >> number) { // Invariant: group and number are not both 0 cout << "Input line specifies that income group " << group << "\ncontains " << number << " people.\n";

if ((group >= LOW_END) && (group <= HIGH_END) && (number >= 0)) // input data is valid -- add it to tally incomeData[index(group)] += number;

else // error in input data: set error flag and // ignore input line dataCorrect = false; } // end for return dataCorrect;} // end readData

Although this input function will behave gracefully in the face of mostcommon input errors, it is not completely fail-safe. What happens if an inputline contains only one integer? What happens if an input line contains a non-integer? The function would be more fail-safe if it read its input character bycharacter, converted the characters to an integer, and checked for end of line. Inmost contexts, this processing would be a bit extreme. However, if the peoplewho enter the data frequently err by typing nonintegers, you could alter theinput function easily because the function is an isolated module. In any case, thefunction’s initial comments should include any assumptions it makes about thedata and an indication of what might make the program abort abnormally.

Carrano_5e.book Page 51 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

52 Chapter 1 Principles of Programming and Software Engineering

Guarding against errors in program logic. Now consider the second type oferror that a program should guard against: errors in its own logic. These areerrors that you may not have caught when you debugged the program or thatyou may have introduced through program modification.

Unfortunately, a program cannot reliably let you know when something iswrong with it. (Could you rely on a program to tell you that something iswrong with its mechanism for telling you that something is wrong?) You can,however, build into a program checks that ensure that certain conditionsalways hold when the program is correctly implementing its algorithm. Asmentioned earlier, such conditions are called invariants.

As a simple example of an invariant, consider again the previous example.All integers in the array incomeData must be greater than or equal to zero.Although the previous discussion argued that the function readData shouldnot check the validity of the entries of incomeData instead of checking number,it could do so in addition to checking number. For example, if the functionfinds that an element in the array incomeData is outside some range ofbelievability, it can signal a potential problem to its users.

Another general way in which you should make a program fail-safe is tomake each function check its precondition. For example, consider the follow-ing function, factorial, which returns the factorial of an integer:

/** Computes the factorial of an integer. * @pre n >= 0. * @post None. * @param n The given integer. * @return n * (n - 1) * ... * 1 if n > 0; * 1 if n == 0. */int factorial (int n){ int fact = 1;

for (int i = n; i > 1; --i) fact *= i;

return fact;} // end factorial

The initial comments in this function contain a precondition—informa-tion about what assumptions are made—as should always be the case. The valuethat this function returns is valid only if the precondition is met. If n is lessthan zero, the function will return the incorrect value of 1.

In the context of the program for which this function was written, it may bereasonable to make the assumption that n will never be negative. That is, if therest of the program is working correctly, it will call factorial only with correctvalues of n. Ironically, this last observation gives you a good reason for facto-rial to check the value of n: If n is less than zero, the warning that results fromthe check indicates that something may be wrong elsewhere in the program.

Functions should check their invariants

Functions should enforce their preconditions

Carrano_5e.book Page 52 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 53

Another reason the function factorial should check whether n is lessthan zero is that the function should be correct outside the context of its pro-gram. That is, if you borrow the function for use in another program, thefunction should warn you if you use it incorrectly by passing it an n that is neg-ative. A stronger check than simply the statement of the precondition in acomment is desirable. Thus, a function should state its assumptions and, whenpossible, check whether its arguments conform to these assumptions.

In this example, factorial could check the value of n and, if it is nega-tive, return zero, because factorials are never zero. The program that usesfactorial could then check for this unusual value.

Alternatively, factorial could abort execution if its argument were nega-tive. Many programming languages, including C++, support a mechanism forerror handling called an exception. A module indicates that an error has occurredby throwing an exception. A module reacts to an exception that another modulethrows by catching the exception and executing code to deal with the error con-dition. Chapter 3 provides more information about exceptions.

C++ also provides a convenient macro assert(expr) that both displaysan informative message and aborts a program if the expression expr is zero.You can use assert to check for both error conditions and the validity of pre-conditions within your program. Appendix C provides more informationabout assert.

DebuggingNo matter how much care you take in writing a program, it will contain errorsthat you need to track down. Fortunately, programs that are modular, clear,and well documented are generally amenable to debugging. Fail-safe tech-niques, which guard against certain errors and report them when they areencountered, are also a great aid in debugging.

Many students seem to be totally baffled by errors in their programs andhave no idea how to proceed. These students simply have not learned to trackdown errors systematically. Without a systematic approach, finding a smallmistake in a large program can indeed be a difficult task.

The difficulty that many people have in debugging a program is perhaps due inpart to a desire to believe that their program is really doing what it is supposed todo. For example, on receiving an execution-time error message at line 1098, astudent might say, “That’s impossible. The statement at line 1098 was not evenexecuted, because it is in the else clause, and I am positive that it was not exe-cuted.” This student must do more than simply protest. The proper approach iseither to trace the program’s execution by using available debugging facilities or toadd cout statements that show which part of the if statement was executed. Bydoing so, you verify the value of the expression in the if statement. If the expres-sion is 0 when you expect it to be 1, the next step is to determine how it became 0.

How can you find the point in a program where something becomes otherthan what it should be? Typically, a programming environment allows you totrace a program’s execution either by single-stepping through the statementsin the program or by setting breakpoints at which execution will halt. You also

Functions should check the values of their arguments

Carrano_5e.book Page 53 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

54 Chapter 1 Principles of Programming and Software Engineering

can examine the contents of particular variables by either establishing watchesor inserting temporary cout statements. The key to debugging is simply to usethese techniques to tell you what is going on. This may sound pretty mun-dane, but the real trick is to use these debugging aids in an effective manner.After all, you do not simply put breakpoints, watches, and cout statements atrandom points in the program and have them report random information.

The main idea is systematically to locate the points of the program thatcause the problem. A program’s logic implies that certain conditions should betrue at various points in the program. (Recall that these conditions are calledinvariants.) If the program’s results differ from your expectations as stated inthe invariants (you did write invariants, didn’t you?), an error occurs. Tocorrect the error, you must find the first point in the program at which thisdifference is evident. By inserting either breakpoints and watches or coutstatements at strategic locations of a program—such as at the entry and depar-ture points of loops and functions—you can systematically isolate the error.

These diagnostic techniques should inform you whether things start goingwrong before or after a given point in the program. Thus, after you run theprogram with an initial set of diagnostics, you should be able to trap the errorbetween two points. For example, suppose that things are fine before you callmethod M1, but something is wrong by the time you call M2. This kind ofinformation allows you to focus your attention between these two points. Youcontinue the process until eventually the search is limited to only a few state-ments. There is really no place in a program for an error to hide.

The ability to place breakpoints, watches, and cout statements in appro-priate locations and to have them report appropriate information comes in partfrom thinking logically about the problem and in part from experience. Hereare a few general guidelines.

Debugging methods. You should examine the values of a method’s argumentsat its beginning and end by using either watches or cout statements. Ideally, youshould debug each major method separately before using it in your program.

Debugging loops. You should examine the values of key variables at thebeginnings and ends of loops, as the comments in this example indicate:

// check values of start and stop before entering loopfor (index = start; index <= stop; ++index){ // check values of index and key variables // at the beginning of iteration . . .

// check values of index and key variables // at the end of iteration} // end for// check values of start and stop after exiting loop

Use breakpoints, single-stepping, watches, and tem-porary cout state-ments to find logic errors

Systematically check a program’s logic to determine where an error occurs

Carrano_5e.book Page 54 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 55

Debugging if statements. Just before an if statement, you should examinethe values of the variables within its expression. You can use either breakpointsor cout statements to determine which branch the if statement takes, as thisexample indicates:

// check variables within expression before executing if if (expression){ cout << "Condition is true (value of expression is 1)."; . . .}

else{ cout << "Condition is false (value of expression is 0)."; . . .} // end if

Using cout statements. Sometimes cout statements can be more conve-nient than watches. Such cout statements should report both the values of keyvariables and the location in the program at which the variables have thosevalues. You can use a comment to label the location, as follows:

// This is point A cout << "At point A in the method computeResults:\n" << "x = " << x << ", y = " << y << endl;

Remember to either disable or remove these statements when your programfinally works.

Using special dump functions. Often the variables whose values you wish toexamine are arrays or other, more complex data structures. If so, you shouldwrite dump functions to display the data structures in a highly readablemanner. You can easily move the single statement that calls each dump func-tion from one point in the program to another as you track down an error.The time you spend on these functions often proves to be worthwhile, as youcan call them repeatedly while debugging different parts of the program.

Hopefully, this discussion has conveyed the importance of the effective useof diagnostic aids in debugging. Even the best programmers have to spendsome time debugging. Thus, to be a truly good programmer, you must be agood debugger.

TestingPrograms need to be tested to make sure that they correctly solve the require-ments of the current iteration. Good testing cannot happen without a plan.Software is complex and determining whether it works correctly is not easy.Software testing is so important that some iterative development processesargue that tests should be developed before writing any code.

You can never test a program too much

Carrano_5e.book Page 55 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

56 Chapter 1 Principles of Programming and Software Engineering

Levels of testing. Several levels of testing should take place. The first level oftesting is called unit testing, and it happens on the individual modules. Youshould test individual methods first, and then test the classes. The second levelof testing is called integration testing, and it tests the interactions amongmodules. The next level of testing is called system testing, where the entireprogram is tested. Finally, a special type of testing called acceptance testingshows that the system as a whole complies with its requirements.

Unit testing is something that you have probably done before. It is closelyrelated to some of the ideas talked about in the debugging section. A functionis tested to make sure that it conforms to its requirements. Often with smallprograms, testing does not go beyond this level.

One way to perform integration tests is to make sure that the variousobjects within a program correctly implement the interactions in yoursequence diagrams. Integration testing can be challenging, because there aremany ways in which the objects in a program can interact. It is difficult tomake sure they do what they are designed to do and nothing more.

System testing entails running the program in the environment in which itwas designed to work. Here it is tested to make sure that it interacts withexternal systems correctly. The external systems might be other programs—such as a government tax collection system—or hardware devices.

Types of testing. Two types of testing can be applied to a system:

■ Open-box testing. With open-box, white-box, or glass-box testing, youdesign tests knowing how the modules work. You carefully choose amethod’s actual arguments in a way that exercises all lines of code in themethod. It is of utmost importance to make sure that all branches of ifand switch statements are executed.

■ Closed-box testing. With closed-box, black-box, or functional testing,you know the name of the method being tested, its argument list, itsreturn type, and its operation contract. Using only this information, youdevelop tests across a range of valid input, and then check the outputagainst hand-calculated results. Choosing some actual arguments outsideof their valid range tests how the module handles incorrect input.

Developing test data. The test data that you use must test a wide range ofconditions. It should include valid, typical input as well as values that shouldnever occur in normal execution. Of utmost importance is to include data thattests boundary values. For example, suppose that we are testing a simplelinear search algorithm:

for (int i = 0; i < size; ++i){ if (array[i] == target) return i;} // end for

You should carefully choose all of the following values of target:

Unit testing checks individual modules

Integration testing checks many parts of a solution together

System testing checks the integra-tion of the solution with other systems

Always carefully test boundary values

Carrano_5e.book Page 56 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Key Issues in Programming 57

■ Just less than index[0] (boundary at the beginning of the array)

■ Equal to index[0] (boundary at the beginning of the array)

■ Equal to some element in the middle of the array

■ Within the range of values of the elements in the array, but unequal to any

■ Equal to index[size – 1] (boundary at the end of the array)

■ Just greater than index[size – 1] (boundary at the end of the array)

You should also test this algorithm with

■ An empty array

■ An array with one element in it

■ An array with many elements

For each of these tests, you should manually trace the data so that the resultsare known ahead of time. In this way, test data can be the input to a module,and you can compare the output with known results.

Testing techniques. As indicated in the debugging section, assert state-ments can be useful for checking invariants. However, the assert statement isnot powerful enough for the full-scale testing of a system. Other techniquesneed to be utilized.

A common technique is to write a block of code that tests a module. Whenyou have finished testing the system, you can simply comment it out—ratherthan deleting it—as more testing will be required after making modificationsduring the next iteration. Alternatively, you can turn these testing blocks ofcode on and off globally by using one of the techniques that we discuss next.

The first technique uses a global Boolean variable to grant access to testingcode contained within the body of if statements. Setting the variable tofalse turns testing code “off,” and setting it to true enables testing. Beaware that when you set the variable to false, the testing code is not removedfrom the executable. It simply disables the guard expression associated with thetesting block. For example:

const bool TESTING = false; // disables testing codeif (TESTING){ // Testing code in here.}

You could define several different Boolean variables to turn different tests“on” and “off.” All that is required is changing the value of the Boolean vari-able and recompiling. The test will be either included or excluded from theexecutable.

A second, more efficient, technique uses the preprocessor to the compiler.You define a macro that guards the testing code. Then you place the test code

Values for testing a search

Disable, but do not remove, testing code after use

Carrano_5e.book Page 57 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

58 Chapter 1 Principles of Programming and Software Engineering

within preprocessor guards that test whether that macro has been defined. Forexample:

#define TESTING

#ifdef TESTING...// Testing code in here.#endif

To turn off testing, you comment out the definition of the macro(// #define TESTING) and recompile. Because the macro is no longer defined,the test code is not included in the executable. This has the advantage that thetest code is completely removed from the executable, making it smaller. Again,you can define several different macros to control turning “on” and “off” dif-ferent blocks of testing code.

Use of stubs. Testing a class before implementing all of its methods is oftenuseful. The body of each unimplemented method acknowledges that it hasbeen called. Such methods are called stubs. Stubs allow you to see that themethod has been called in the correct place in the overall flow of a program.Here is an example of a stub:

class Data{public: Data() { data[0] = 2; data[1] = 3; data[2] = 1; } // end constructor

void sortData() // stub { cout << "Data::sortData has been called\n" data[0] = 1; data[1] = 2; data[2] = 3; } // end sortData

void dump() { for (int i = 0; i < 3; ++i) cout << "data[" << i << "] = " << data[i] << endl; } // end dump

private: int data[3];}; // end Data

int main(){ Data t; t.dump();

A stub is an imcom-plete method that acknowledges that it has been called

sortData is a stub

Carrano_5e.book Page 58 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Summary 59

t.sortData(); t.dump();

return 0;} // end main

The stub sortData not only indicates that it has been called, but alsochanges the data into a sorted form. Thus, this stub could be used for simpleintegration testing as well. Later, you could implement sortData by using anappropriate sorting algorithm.

Use of drivers. The previous example also has a main function that creates aData object and then calls its methods. First, it dumps the object, then it sortsits data, and finally dumps it again. (Notice the use of a dump method as out-lined in the debugging section.) This main function is called a driver. It doesnot do anything algorithmically interesting; it simply tests a Data object.

As is evident from the preceding discussion, testing takes a lot of planningand is not easy to do. Like many things in programming, it takes some timeand experience to test well.

1. Software engineering is a branch of computer science that studies ways to facilitatethe development of computer programs.

2. The life cycle of software describes the phases through which software will progress,from conception to replacement to deletion from a hard disk forever.

3. A loop invariant is a property of an algorithm that is true before and after each iter-ation of a loop. Loop invariants are useful in developing iterative algorithms andestablishing their correctness.

4. When evaluating the quality of a solution, you must consider a diverse set of fac-tors: the solution’s correctness, its efficiency, the time that went into its develop-ment, its ease of use, and the cost of modifying and expanding it.

5. For problems that primarily involve data management, encapsulate data with oper-ations on that data by designing classes. The nouns in the problem statement canhelp you identify appropriate classes. Break algorithmic tasks into independent sub-tasks that you gradually refine. In all cases, practice abstraction; that is, focus onwhat a module does instead of how it does it.

6. UML is a modeling language used to express object-oriented designs. It provides anotation to specify the data and operations and uses diagrams to show relationshipsamong classes.

7. Take great care to ensure that your final solution is as easy to modify as possible.Generally, a modular program is easy to modify because changes in the program’srequirements frequently affect only a handful of the modules. Programs should notdepend on the particular implementations of its modules.

A driver is a module that tests another module

Summary

Carrano_5e.book Page 59 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

60 Chapter 1 Principles of Programming and Software Engineering

8. A function or method should be as independent as possible and perform one well-defined task.

9. A function or method should always include an initial comment that states its pur-pose, its precondition—that is, the conditions that must exist at the beginning of amodule—and its postcondition—the conditions at the end of a module.

10. A program should be as fail-safe as possible. For example, a program should guardagainst errors in input and errors in its own logic. By checking invariants—whichare conditions that are true at certain points in a program—you can monitorcorrect program execution.

11. The effective use of available diagnostic aids is one of the keys to debugging. Youshould use watches or cout statements to report the values of key variables at keylocations. These locations include the beginnings and ends of functions and loops,and the branches of selection statements.

12. To make it easier to examine the contents of arrays and other, more complex datastructures while debugging, you should write dump functions that display the con-tents of the data structures. You can easily move calls to such functions as you trackdown an error.

1. Your programs should guard against errors. A fail-safe program checks that an inputvalue is within some acceptable range and reports if it is not. An error in input shouldnot cause a program to terminate before it clearly reports what the error was. A fail-safe program also attempts to detect errors in its own logic. For example, in many sit-uations functions should check that their arguments have valid values.

2. You can write better, correct programs in less time if you pay attention to the fol-lowing guidelines: Write precise specifications for the program. Use a modulardesign. Write pre- and postconditions for each function before you implement it.Use meaningful identifiers and consistent indentation. Write comments, includingassertions and invariants.

The answers to all Self-Test Exercises are at the back of this book.

1. What is the loop invariant for the following?

int index = 0;int sum = item[0];

while (index < n){ ++index; sum += item[index];} //end while

2. Write specifications using UML notation for a function that computes the sum ofthe first five positive integers in an array of n arbitrary integers.

Cautions

Self-Test Exercises

Carrano_5e.book Page 60 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Exercises 61

1. The price of an item you want to buy is given in dollars and cents. You pay for it incash by giving the clerk d dollars and c cents. Write specifications for a functionthat computes the change, if any, that you should receive. Include a statement ofpurpose, the pre- and postconditions, and a description of the arguments.

2. A date consists of a month, day, and year. Frequently, we represent each of theseitems as integers. For example, July 4, 1776, is month 7, day 4, and year 1776.

a. Write specifications for a function that advances any given date by one day.Include a statement of purpose, the pre- and postconditions, and a descriptionof the arguments.

b. Write a C++ implementation of this function. Design and specify any otherfunctions that you need. Include comments that will be helpful to someonewho will maintain your implementation in the future.

3. Consider the following program, which interactively reads and writes the identifica-tion number, age, salary (in thousands of dollars), and name of each individual in agroup of employees. How can you improve the program? Some of the issues areobvious, others are more subtle. Try to keep in mind all of the topics discussed inthis chapter.

int main(){ int x1, x2, x3, i; char name[8];

for (cin >> x1 >> x2 >> x3; x1 != 0; cin >> x1 >> x2 >> x3) { for (i = 0; i < 8; ++i) cin >> name[i];

cout << x1 << x2 << x3 << endl;

for (i = 0; i < 8; ++i) cout << name[i]; cout << endl; } // end for

return 0;} // end main

4. What is the problem with the following code fragment?

num = 50;while (num >= 0){ cout << num << endl; num = num + 1;} // end while

Exercises

Carrano_5e.book Page 61 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

62 Chapter 1 Principles of Programming and Software Engineering

5. This chapter stressed the importance of adding fail-safe checks to a program wher-ever possible. What can go wrong with the following function? How can youprotect yourself?

double compute(double x){ return sqrt(x)/cos(x);} // end compute

6. Write the loop invariants for the function factorial, which appears in the section“Fail-Safe Programming.”

7. Write the loop invariant for the following:

int i = 20;while (i > 1) i--;

8. Using a for loop, write a program that displays the squares of all integers greaterthan 0 and less than or equal to a given number n.

9. Write the function that Self-Test Exercise 2 describes, and state the loop invariants.

10. By using loop invariants, demonstrate that the algorithm in Self-Test Exercise 1correctly computes item[0] + item[1] + · · · + item[n].

11. The following program is supposed to compute the floor of the square root of itsinput value x. (The floor of a number n is the largest integer less than or equal to n.)

// Computes and writes floor(sqrt(x)) for// an input value x >= 0.int main(){ int x; // input value

// initialize int result = 0; // will equal floor of sqrt(x) int temp1 = 1; int temp2 = 1;

cin >> x; // read input

// compute floor while (temp1 < x) { ++result; temp2 += 2; temp1 += temp2; } // end while

cout << "The floor of the square root of " << x << " is " << result << endl; return 0;} // end main

This program contains an error.

Carrano_5e.book Page 62 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

Programming Problems 63

a. What output does the program produce when x = 64?

b. Run the program and remove the error. Describe the steps you took to find theerror.

c. How can you make the program more user friendly and fail-safe?

12. Suppose that, due to some severe error, you must abort a program from a locationdeep inside nested function calls, while loops, and if statements. Write a diagnos-tic function that you can call from anywhere in a program. This function shouldtake an error code as an argument (some mnemonic enumeration), display anappropriate error message, and terminate program execution.

1. Add a Transaction class to the banking example in Figure 1-5. This class keepstrack of the date, time, amount, and type of transaction (checking or savings).

2. Consider a program that will read employee information into an array of structures,sort the array by employee identification number, write out the sorted array, andcompute various statistics on the data, such as the average age of an employee. Writecomplete specifications using UML notation for this problem and design a modularsolution. What functions did you identify during the design of your solution? Writespecifications, including preconditions and postconditions, for each function.

3. Revise the UML diagram in Programming Problem 2 to reflect an object-orienteddesign. For example, instead of using an array of structures, you might designEmployee and EmployeeStats classes with the appropriate functions.

4. Write a program that sorts and evaluates bridge hands. The input is a stream ofcharacter pairs that represent playing cards. For example,

2C QD TC AD 6C 3D TD 3H 5H 7H AS JH KH

represents the 2 of clubs, queen of diamonds, 10 of clubs, ace of diamonds, and soon. Each pair consists of a rank followed by a suit, where rank is A, 2, . . . , 9, T, J,Q, or K, and suit is C, D, H, or S. You can assume each input line representsexactly 13 cards and is error-free. Input is terminated by an end of file.

For each line of input, form a hand of 13 cards. Display each hand in a readableform arranged both by suits and by rank within suit (aces are high). Then evaluatethe hand by using the following standard bridge values:

Aces count 4

Kings count 3

Queens count 2

Jacks count 1

Voids (no cards in a suit) count 3

Singletons (one card in a suit) count 2

Doubletons (two cards in a suit) count 1

Long suits with more than 5 cards in the suit count 1 for each card over 5 in number

Programming Problems

Carrano_5e.book Page 63 Thursday, June 15, 2006 6:09 PM

ISB

N: 0

-558

-138

56-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.

64 Chapter 1 Principles of Programming and Software Engineering

For example, for the previous sample input line, the program should produce theoutput

CLUBS 10 6 2DIAMONDS A Q 10 3HEARTS K J 7 5 3SPADES APoints = 16

because there are two aces, one king, one queen, one jack, one singleton, no dou-bletons, and no long suits. (The singleton ace of spades counts as both an ace anda singleton.)

Optional: See how much more flexible and fail-safe you can make your program.That is, try to remove as many of the previous assumptions in input as you can.

5. Write a program that will act as an interactive calculator capable of handling verylarge (larger than the largest long integer) nonnegative integers. This calculatorneed perform only the operations of addition and multiplication.

In this program, each input line is of the form

num1 op num2

and should produce output such as

num1op num2--------

num3

where num1 and num2 are (possibly very large) nonnegative integers, op is the singlecharacter + or *, and num3 is the integer that results from the desired calculation.

Design your program carefully. You will need the following:

■ A data structure to represent large numbers: for example, an array of digits in anumber.

■ A function to read in numbers. Skip leading zeros. Do not forget that zero is avalid number.

■ A function to write numbers. Do not write leading zeros, but if the numberconsists of all zeros, write a single zero.

■ A function to add two numbers.

■ A function to multiply two numbers.

In addition, you should:

■ Check for overflow (numbers with more than MAX_SIZE digits) when reading,adding, and multiplying numbers.

■ Have a good user interface.

Optional: Allow signed integers (negative as well as positive integers) and write afunction for subtraction.

Carrano_5e.book Page 64 Thursday, June 15, 2006 6:09 PM

ISB

N: 0-558-13856-X

Data Abstraction & Problem Solving with C++, Fifth Edition, by Frank M. Carrano. Published by Addison-Wesley. Copyright © 2007 by Pearson Education, Inc.