Let us generalize Section 5.9’s class-average problem. Consider the following problem:
Develop a class-averaging app that processes grades for an arbitrary number of students each time it’s run.
In the previous class-average example, the problem statement specified the number of students, so the number of grades (10) was known in advance. In this example, no indication is given of how many grades the user will enter during the program’s execution. The program must process an arbitrary number of grades. How can it determine when to stop the input of grades? How will it know when to calculate and print the class average?
One way to solve this problem is to use a special value called a sentinel value (also called a signal value, a dummy value or a flag value) to indicate “end of data entry.” The user enters grades until all legitimate grades have been entered. The user then types the sentinel value to indicate that no more grades will be entered. Sentinel-controlled iteration is often called indefinite iteration because the number of iterations is not known before the loop begins executing. Clearly, a sentinel value must be chosen that cannot be confused with an acceptable input value. Grades on a quiz are nonnegative integers, so –1 is an acceptable sentinel value for this problem. Thus, a run of the class-averaging program might process a stream of inputs such as 95, 96, 75, 74, 89 and –1. The program would then compute and print the class average for the grades 95, 96, 75, 74 and 89; since –1 is the sentinel value, it should not enter into the averaging calculation.
We approach this class-averaging program with a technique called top-down, stepwise refinement, which is essential to the development of well-structured programs. We begin with a pseudocode representation of the top—a single statement that conveys the overall purpose of the program:
Determine the class average for the quiz
The top is, in effect, a complete representation of a program. Unfortunately, the top rarely conveys sufficient detail from which to write a C# app. So we now begin the refinement process. We divide the top into a series of smaller tasks and list these in the order in which they’ll be performed. This results in the following first refinement:
Initialize variables
Input, sum and count the quiz grades
Calculate and print the class average
This refinement uses only the sequence structure—the steps listed should execute in order, one after the other.
Each refinement, as well as the top itself, is a complete specification of the algorithm— only the level of detail varies.
Many programs can be divided logically into three phases: an initialization phase that initializes the variables; a processing phase that inputs data values and adjusts program variables accordingly; and a termination phase that calculates and outputs the final results. Software Engineering Observation 5.3
The preceding Software Engineering Observation is often all you need for the first refinement in the top-down process. To proceed to the next level of refinement—that is, the second refinement—we commit to specific variables. In this example, we need a running total of the numbers, a count of how many numbers have been processed, a variable to receive the value of each grade as it’s entered by the user and a variable to hold the calculated average. The pseudocode statement
Initialize variables
can be refined as follows:
Initialize total to zero
Initialize counter to zero
Only the variables total and counter need to be initialized before they’re used. The variables average and grade (for the calculated average and the user input, respectively) need not be initialized, because their values will be replaced as they’re calculated or input.
The pseudocode statement
Input, sum and count the quiz grades
requires iteration to successively input each grade. We do not know in advance how many grades will be entered, so we’ll use sentinel-controlled iteration. The user enters grades one at a time. After entering the last grade, the user enters the sentinel value. The program tests for the sentinel value after each grade is input and terminates the loop when the user enters the sentinel value. The second refinement of the preceding pseudocode statement is then
Prompt the user to enter the first grade
Input the first grade (possibly the sentinel)
While the user has not yet entered the sentinel
Add this grade into the running total
Add one to the grade counter
Prompt the user to enter the next grade
Input the next grade (possibly the sentinel)
We simply indent the statements under the While to show that they belong to the While. Again, pseudocode is only an informal program-development aid.
The pseudocode statement
Calculate and print the class average
can be refined as follows:
If the counter is not equal to zero
Set the average to the total divided by the counter
Print the average
Else
Print “No grades were entered”
We’re careful here to test for the possibility of division by zero—a logic error that, if undetected, would cause the program to fail or produce invalid output. The complete second refinement of the pseudocode for the class-average problem is shown in Fig. 5.10.
When performing division by an expression whose value could be zero, explicitly test for this possibility and handle it appropriately in your app (e.g., by displaying an error message) rather than allowing the error to occur.
In Fig. 5.8 and Fig. 5.10, we included blank lines and indentation in the pseudocode to make it more readable. The blank lines separate the algorithms into their phases and set off control statements; the indentation emphasizes the bodies of the control statements.
The pseudocode algorithm in Fig. 5.10 solves the more general class-average problem. This algorithm was developed after two refinements. Sometimes more are needed.
Terminate the top-down, stepwise refinement process when you’ve specified the pseudocode algorithm in sufficient detail for you to convert the pseudocode to C#.
Some programmers do not use program-development tools like pseudocode. They feel that their ultimate goal is to solve the problem on a computer and that writing pseudocode merely delays the production of final outputs. Although this may work for simple and familiar problems, it can lead to serious errors and delays in large, complex projects. Software Engineering Observation 5.5
In Fig. 5.11, method Main
implements the pseudocode algorithm of Fig. 5.10. Although each grade entered by the user is an integer, the averaging calculation is likely to produce a number with a decimal point—in other words, a real number or floating-point number (e.g., 7.33, 0.0975 or 1000.12345). The type int
cannot represent such a number, so this example must use another type to do so. C# provides data types float
and double
to store floating-point numbers in memory. The primary difference between these types is that double
variables can typically store numbers with larger magnitude and finer detail (i.e., more digits to the right of the decimal point—also known as the number’s precision). We say more about floating-point types in Chapter 6.
Recall that integer division produces an integer result. This program introduces a special operator called a cast operator to force the averaging calculation to produce a floating-point numeric result. This program also stacks control statements on top of one another (in sequence)—the while
statement (lines 19–27) is followed in sequence by an if
…else
statement (lines 31–44). Much of the code in this program is identical to that in Fig. 5.9, so we concentrate on the new concepts.
Line 11 initializes gradeCounter
to 0
, because no grades have been entered yet. Remember that this program uses sentinel-controlled iteration to input the grades. The program increments gradeCounter
only when the user enters a valid grade. Line 34 declares double
variable average
, which stores the class average as a floating-point number.
Compare the program logic for sentinel-controlled iteration in this program with that for counter-controlled iteration in Fig. 5.9. In counter-controlled iteration, each iteration of the while
statement (lines 14–20 of Fig. 5.9) reads a value from the user, for the specified number of iterations. In sentinel-controlled iteration, the program prompts for and reads the first value (lines 15–16 of Fig. 5.11) before reaching the while
. This value determines whether the program’s flow of control should enter the body of the while
. If the condition of the while
is false
(line 19), the user entered the sentinel value, so the body of the while
does not execute (i.e., no grades were entered). If, on the other hand, the condition is true
, the body begins execution, and the loop adds the grade
value to the total
and increments the gradeCounter
(lines 21–22). Then lines 25–26 in the loop body input the next value from the user. Next, program control reaches the closing right brace of the loop body at line 27, so execution continues with the test of the while
’s condition (line 19). The condition uses the most recent grade
entered by the user to determine whether the loop body should execute again.
The value of variable grade
is always input from the user immediately before the program tests the while
condition. This allows the program to determine whether the value just input is the sentinel value before the program processes that value (i.e., adds it to the total
). If the sentinel value is input, the loop terminates, and the program does not add –1 to the total
.
In a sentinel-controlled loop, prompts should remind the user of the sentinel.
After the loop terminates, the if
…else
statement at lines 31–44 executes. Line 31 determines whether any grades were input. If none were input, the if
…else
statement’s else
part executes and displays the message "No grades were entered"
.
while
StatementNotice the while
statement’s block in Fig. 5.11 (lines 20–27). Without the braces, the loop would consider its body to be only the first statement, which adds the grade
to the total
. The last three statements in the block would fall outside the loop’s body, causing the computer to interpret the code incorrectly as follows:
while (grade != -1)
total = total + grade; // add grade to total
gradeCounter = gradeCounter + 1; // increment counter
// prompt for input and read grade from user
Console.Write("Enter grade or -1 to quit: ");
grade = int.Parse(Console.ReadLine());
The preceding code would cause an infinite loop if the user did not enter the sentinel -1
at line 16 (before the while
statement).
Omitting the braces that delimit a block can lead to logic errors, such as infinite loops. To prevent this and other problems, we always enclose the body of every control statement in braces even if the body contains only a single statement.
If at least one grade was entered, line 34 of Fig. 5.11
double average = (double) total / gradeCounter;
calculates the average. Recall from Fig. 5.9 that integer division yields an integer result. Even though variable average
is declared as a double
, if we had written line 34 as
double average = total / gradeCounter;
it would lose the fractional part of the quotient before the result of the division was used to initialize average
.
To perform a floating-point calculation with integers in this example, you first create a temporary floating-point value using the unary cast operator. Line 34 uses the (
double
)
unary cast operator—which has higher precedence than the arithmetic operators—to create a temporary double
copy of its operand total
, which appears to the right of the operator. The value stored in total
is still an integer. Using a cast operator in this manner is called explicit conversion.
After the cast operation, the calculation consists of the temporary double
copy of total
divided by the integer gradeCounter
. For arithmetic, the compiler knows how to evaluate only expressions in which the operand types are identical. To ensure this, the compiler performs an operation called promotion (also called implicit conversion) on selected operands. In an expression containing values of types int
and double
, the compiler promotes int
operands to double
values. So, in line 34, the compiler creates a temporary copy of gradeCounter
’s value of type double
, then performs the floating-point division. Finally, average
is initialized with the floating-point result. Section 7.6.1 discusses the allowed simple-type promotions.
Cast operators are available for all simple types. We’ll discuss cast operators for other types in Chapter 12. The cast operator is formed by placing parentheses around the name of a type. This operator is a unary operator—it takes only one operand. C# also supports unary versions of the plus (+
) and minus (–
) operators, so you can write expressions like +5
or -7
. Cast operators have the second highest precedence. (See the operator precedence chart in Appendix A.)
Line 39
Console.WriteLine($"Class average is {average:F}");
outputs the class average. In this example, we decided that we’d like to display the class average rounded to the nearest hundredth and output the average with exactly two digits to the right of the decimal point. The format specifier F
in the interpolation expression
{average:F}
typically formats average
’s value with two digits to the right of the decimal point—again, the Windows culture settings on the user’s machine determine the actual format, including the digits to the right of the decimal point, whether commas or periods are used for separating thousands, millions, etc.
When the F
format specifier is used to format a floating-point value, the formatted value is rounded to a specific decimal position, although the value in memory remains unaltered. In many cultures a floating-point value output with F
will be rounded to the hundredths position—for example, 123.457 will be rounded to 123.46, and 27.333 will be rounded to 27.33—though in some cultures these values are rounded to whole numbers. In this app, the three grades entered during the sample execution total 257, which yields the average 86.66666…. In the United States, the F
format specifier rounds average
to the hundredths position, so the average is displayed as 85.67
.