You need to make a little change to the user interface of the calculator, to make it more “testable.”
Instead of prompting users to input numbers or indicate that they want to stop, the program will now get commands from a “little language.” These commands will be typed into the running program when it needs input—just as you type on your handheld calculator.
A statement in this language has the following form:
<operator><operand>
<operator> can be any of the following:
+ to add the operand to the accumulator
- to subtract the operand from the accumulator
* to multiply the accumulator by the operand
/ to divide the accumulator by the operand
@ to set the accumulator to a specific value
= to show the current value in the accumulator
? to show the tape
An entire set of operators and operands can be entered on a single line (for example, +3-2*12/3=, which outputs 4). Note that our little language has no precedence and performs its steps strictly from left to right (0+3 = 3; 3-2 = 1; 1*12 = 12; 12/3 = 4).
An operand is optional for the =, ?, !, and . operators.
The main() function has a few changes, as shown in Listing 14.1.
Line 5 moves the variable Operator outside the loop because its value will be used to stop the loop, as seen in line 47. |
Lines 13–21 identify and get the operands for operators that have them, and then give them to the accumulator.
You can see that the name of the Accumulate() function has been changed to Accumulator(). Naming a function with a noun instead of a verb indicates that it has an internal state and that it does not depend solely on the arguments passed in any specific call.
Lines 25–28 run the self-test.
Line 29 makes sure that the program does nothing when the . operator is entered. The empty block makes this clear.
Lines 33–36 are for any operator with no operand. This call on the Accumulator() has only one parameter.
The Accumulator() shown in Listing 14.2 has some new lines in its switch statement, because a much smaller percentage of its operators are recorded on the Tape().
Line 1 has one of the most substantial changes. It now has the Accumulator() returning the current value of myAccumulator on line 51. |
There is a new feature shown in the formal argument theOperand on line 1. An equal sign and a zero follow the formal argument name. This means that the formal argument is an optional argument, and if the actual argument is not provided in a function call, the argument will be given a default value—in this case 0. The default value lets the compiler know that the call in line 49 of main() is allowed.
The other new lines are straightforward—they simply add calls to Tape() for every operator whose action is to be recorded, or implement new operators, such as setting the accumulator value on lines 31–35 and displaying the accumulator value on lines 37–39.
Getting the operator and operand requires a minor change—as you can see in Listing 14.3, the prompt has been removed.
The SelfTest function (see Listing 14.4) is performed by line 27 of main() when you enter ! as input. It runs a test of the accumulator.
Line 3 saves the value of the accumulator, which is restored in Line 28.
Lines 7–14 actually execute the tests. This block tests every operator that changes the accumulator by calling the TestOK() function. Because this uses the relational operator &&, if any tests fail, the self-test fails.
SelfTest() uses TestOK() (see Listing 14.5) to determine whether each operator/operand submitted to the Accumulator() provides the expected result.
This is a function that has a bool result and three const arguments—the operator, the operand, and the result expected from the accumulator. The function performs the operation and reports whether or not the result matches what is expected.
Test functions like this and SelfTest() must be kept simple. You need to be able to validate test functions by eye (by performing what is called a walkthrough). If they are wrong, you might hunt for bugs (runtime errors) that don't exist or miss ones that do.
You only need to change one line in the Tape() function to aid testing.
Convert
*3: static const int myTapeChunk = 20;
to
*3: static const int myTapeChunk = 3;
Why? To verify that heap allocations are working, testing must provoke Tape() to resize the tape at least once. This is done by making the chunk size smaller than the number of operations in the test.
It's time to look for errors. Let's run a self-test:
1: !
2:
3: 0
4: @0 - succeeded.
5: +3 - succeeded.
6: -2 - succeeded.
7: *4 - failed. Expected 4, got 1
8: Test failed.
Test line 1 shows the self-test operator being input to the program—type it and press Enter. Line 3 shows the value of the accumulator being saved by the test, as a result of line 3 in SelfTest(). The tests succeed until output line 7.
Notice that only three of the four tests have run. Why didn't the division test run?
One of the nice things about the && relational operator is that it uses what is called short-circuit evaluation. This means that as soon as any expression connected by && is false, subsequent expressions will not be executed. And why should they? As soon as any expression is false, the entire expression will be false.
Further tests are not performed if any of them fail, because each test's expected result requires a correct result from the prior test. But short- circuit evaluation can be a problem if you expect a function in such an expression to execute regardless of the outcome of prior expressions.
Fortunately, the self-test points you right to the error line:
19: case '*': 20: 21: myAccumulator = myAccumulator,theOperand;
You will notice that the program is not multiplying in line 21. Instead of a *, there is a ,. This was not a contrived error. I actually made this error while writing the example and discovered it in the self-test.
You might wonder why the compiler lets you make this mistake. It turns out that the , is the infix comma operator. It returns the value of the rightmost operand as its result. There are some uses for this, but it is very uncommon.
Once you've fixed the error and rerun the program, you will see that the program works fine. Even the Tape() reallocations seem to have no problem. But be skeptical. Heap allocation errors can hide in even the best code and not show up in testing.
18.188.35.158