The glasses are unmarked; otherwise, it would be easy to pour water from the large, full glass into the medium-sized glass (i.e., the 5-ounce glass) until it contained 4 ounces (which would also leave 4 ounces in the large glass). So, the only way to "measure" the water is to fill the smaller glasses.
Our overall problem is to write a program that simulates a person pouring water from one glass to another while attempting to solve the puzzle. We take the common approach of breaking the overall problem down into a series of smaller steps. The first step is to find a suitable representation for the puzzle problem.
Pictures help us organize the problem's data and identify the missing information. They also make it easier to see the relations between the different parts of the problem. They needn't be "pretty," accurate, or drawn to scale. Although drawing representations of specific situations occurring within a problem might be beneficial, a picture representing a general situation or condition is frequently more helpful. To that end, we begin by drawing a picture of two arbitrary glasses that do not distinguish between the three different-sized glasses and labeling the various parts of the problem. The following figure generally represents any two glasses joined by the pouring process.
destination.pour(source);
The glass playing the "destination" role can also be called "this" glass. The direction in which we pour the water is entirely arbitrary, and we could redefine the pour function so that the "destination" class becomes the function argument and the "source" glass becomes "this" glass. Every glass has two attributes that fully describe or characterize it:
There is also some empty space at the top of a glass when the glass is not full. We could also make space a class attribute, but the program can quickly calculate the value when needed. Furthermore, if we make space an attribute, we must be careful to update it whenever the pour operation changes it. The picture makes the relationship between the three values quite clear:
space = volume ‑ amount
The next step is to solve the problem abstractly and develop an algorithm suitable for programming.
Previously, we examined three phases that are commonly included in software engineering processes: analysis, design, and implementation. As we analyze the pouring puzzle, we note that the problem centers around three objects: a 3-ounce glass, a 5-ounce glass, and an 8-ounce glass. All three glasses have the same attributes: volume and the amount of water currently in the glass.
All three glasses are similar because they represent the same kind of "thing" and have identical attributes. The volume differentiates one glass object from the others. Therefore, we can abstract or generalize the three glass objects into a single Glass class.
In practice, classes described by a UML class diagram are language-independent. We can translate a UML class to Java, C#, or any object-oriented programming language as easily as we can to C++. It is the task of the programmer to add any needed language-specific detail. But, as we are still learning the subtleties of C++, two slightly different versions of the Glass class are presented. The only difference between the two versions is the pour function argument (i.e., the argument appearing inside the parentheses).
![]() |
![]() |
Pass-by-reference | Pass-by-pointer |
---|
The pour operation changes the amount of water in the destination and source glasses, which it can only do if the program passes the objects by pointer or reference. (Please see Function Argument Passing Summary.) The destination or implicit object is bound to the pour function through the this
pointer, which means that it is always passed-by-pointer. But, we can choose how to pass the source or explicit object. The way that we pass the explicit argument to the pour function is the only difference between the two UML diagrams. We generally don't show such implementation details on UML diagrams, which are language-independent. I've included the passing notation in the diagrams to help prepare us for the two solutions presented in the following sections.
A program is just an expression of an existing solution or algorithm. It's okay to write bits of code to test the various steps in a solution, but writing code without a starting and ending point and some idea of the path between the two is slow, frustrating, and unproductive. We begin developing the problem solution by noting that the relation between the space in the destination class and the amount of water in the source glass determines how much water the pour operation can transfer from one glass into the other. There are two cases to consider:
Greeno, Collins, & Resnick (1996) report that "Research comparing excellent adult learners with less capable ones ... [confirms] that the most successful learners elaborate what they read" (p. 19). For computer programming students, one of the elaborating steps is asking, "Is there more than one way to write the code that solves a problem?" The following figure illustrates three ways of writing code to express the solution developed from the two cases above. How we choose to look at a problem affects our solution. The three code fragments represent the same algorithm viewed from slightly different perspectives.
int space = volume - amount; if (space < source.amount) { source.amount -= space; amount = volume; } else { amount += source.amount; source.amount = 0; } |
(a) |
int space = volume - amount; int transfer = (space < source.amount) ? space : source.amount; amount += transfer; source.amount -= transfer; |
(b) |
int space = volume - amount; int transfer = min(space, source.amount); amount += transfer; source.amount -= transfer; |
(c) |
We can create a class named Glass that defines a pour function. We can create a pouring-puzzle program using three instances of the Glass class.
Glass g1; Glass g2; Glass g3; |
destination.pour(source); |
g1.pour(g2); g2.pour(g1); g1.pour(g3); g3.pour(g1); g2.pour(g3); g3.pour(g2); |
(a) | (b) | (c) |
if (destination == 1 && source == 2) g1.pour(g2); else if (destination == 2 && source == 1) g2.pour(g1); . . .
A more elegant, more compact, and more easily programmed solution is desirable.
Glass glasses[3]; while (the puzzle is not solved) { int source = user input; int destination = user input; . . . glasses[destination].pour(glasses[source]); }
Array definition | Passing the source glass | |
---|---|---|
by reference | by pointer | |
Glass glasses[3]...; | glasses[d].pour(glasses[s]); | glasses[d].pour(&glasses[s]); |
Glass* glasses[]...; | glasses[d]->pour(*glasses[s]); | glasses[d]->pour(glasses[s]); |
Roughly outlining the overall program logic is the final step before programming, which we do with the following logic diagram.
Two similar program versions are developed in the following sections. The first program creates an automatic array of Glass objects on the stack. The second program creates a dynamic array on the heap with the new
operator.