The declaration of the Game object builds on the material we've covered so far, and it adds a few new elements you'll need to build a robust class. Let's start by taking a look at the code (see Listing 5.1) and then discussing it in detail.
1: #ifndef GAME_H 2: #define GAME_H 3: 4: #include "definedValues.h" 5: 6: class Game 7: { 8: public: 9: Game(); 10: ~Game() {} 11: void Display(const char * charArray) const 12: { 13: cout << charArray << endl; 14: } 15: void Play(); 16: const char * GetSolution() const 17: { 18: return solution; 19: } 20: void Score(const char * thisGuess, int & correct, int & position); 21: 22: private: 23: int howMany(const char *, char); 24: char solution[maxPos]; 25: int howManyLetters; 26: int howManyPositions; 27: int round; 28: bool duplicates;
29: };Once again, you see inclusion guards on line 1, and you now see the naming pattern that I'll use throughout this book. The inclusion guard will typically have the name of the class or file, in all uppercase, followed by the underscore (_) and the uppercase letter H. By having a standard for the creation of inclusion guards, you can reduce the likelihood of using the same guard name on two different header files.
On line 4, we include definedValues.h. As promised, this file will be included throughout the program.
On line 6, we begin the declaration of the Game class. In the public section we see the public interface for this class. Note that the public interface contains only methods, not data; in fact, it contains only those methods that we want to expose to clients of this class.
On line 9, we see the default constructor, as described in Chapter 4, "Creating Classes," and on line 10, we see the destructor. The destructor, as it is shown here, has an inline implementation, as do Display() (lines 11 to 14) and GetSolution (lines 16 to 19).
Normally, when a function is called, processing literally jumps from the calling function to the called function.
The processor must stash away information about the current state of the program. It stores this information in an area of memory known as the stack, which is also where local variables are created.
NOTE: The stack is an area of memory in which local variables and other information about the state of the program are stored.
The processor must also put the parameters to the new function on the stack and adjust the instruction pointer (which keeps track of which instruction will execute next), as illustrated in Figure 5.1. When the function returns, the return value must be popped off the stack, the local variables and other local state from the function must be cleaned up, and the registers must be readjusted to return you to the state you were in before the function call.
Figure 5.1 The instruction pointer.
An alternative to a normal function call is to define the function with the keyword inline. In this case, the compiler does not create a real function: It copies the code from the inline function directly into the calling function. No jump is made. It is just as if you had written the statements of the called function right into the calling function.
Note that inline functions can bring a heavy cost. If the function is called 10 times, the inline code is copied into the calling functions each of those 10 times. The tiny improvement in speed you might achieve is more than swamped by the increase in size of the executable program. Even the speed increase might be illusory. First, today's optimizing compilers do a terrific job on their own, and there is almost never a big gain from declaring a function inline. More importantly, though, the increased size brings its own performance cost.
If the function you are calling is very small, it might still make sense to designate that function as inline. There are two ways to do so. One is to put the keyword inline into the definition of the function, before the return value:
inline int Game::howMany()
An alternative syntax for class member methods is just to define the method right in the declaration of the class itself. Thus, on line 19 you might note that the destructor's implementation is defined right in the declaration. It turns out that this destructor does nothing, so the braces are empty. This is a perfect use of inlining: There is no need to branch out to the destructor, just to take no action.
Take a look at Display on lines 11-14. After the argument list is the keyword const. This use of const means, "I promise that this method does not change the object on which you invoke this method," or, in this case, "I promise that Display() won't change the Game object on which you call Display()."
const methods attempt to enlist the compiler in helping you to enforce your design decisions. If you believe that a member method ought not change the object (that is, you want the object treated as if it were read-only), declare the method constant. If you then change the object as a result of calling the method, the compiler flags the error, which can save you from having a difficult-to-find bug.
On line 20, we see the signature for the member method Score(). The signature of a method is the name (Score) and the parameter list. The declaration of a method consists of its signature and its return value (in this case, void).
signature--The name and parameter list of a method.
Before we examine this signature in detail, let's talk about what this method does and what parameters it needs. The responsibility of this method is to examine the player's guess (an array of characters) and to score it on how many letters he correctly found, and of those letters, how many were in the right place.
Let's take a look at how this will be used. Listing 5.2 shows the Play() method, which calls Score().
0: void Game::Play() 1: { 2: char guess[80]; 3: int correct = 0; 4: int position = 0; 5: 6: //... 7: cout << "\nYour guess: "; 8: Display(guess); 9: 10: Score(guess,correct,position); 11: cout << "\t\t" << correct << " correct, " << position 12: << " in position." << endl; 13: }
I've elided much of this method (indicated by the //... marks), but the code with which we are concerned is shown. We ask the user for his guess and store it in the character array guesson line 2. We display guess by calling Display() on line 8 and passing guess in as a parameter. We then we score it by calling Score() on line 10 and passing in guess and two integer variables: correct (declared on line 3) and position (declared on line 4). Finally, we print the values for correct and position on lines 11 and 12.
Score() adjusts the values of correct and position. To accomplish this, we must pass in these two variables by reference.
When you pass an object into a method as a parameter to that method, you can pass it either by reference or by value. If you pass it by reference, you are providing that function with access to the object itself. If you pass it by value, you are actually passing in a copy of the object.
passing by reference--Passing an object into a function.
passing by value--Passing a copy of an object into a function.
This distinction is critical. If we pass correct and position by value, Score() cannot make changes to these variables back in Play(). Listing 5.3 illustrates a very simple program that shows this problem.
0: #include <iostream> 1: using namespace std; 2: 3: class Game 4: { 5: public: 6: Game(){}; 7: ~Game(){} 8: void Play(); 9: void Score(int correct, int position); 10: 11: private: 12: int howManyLetters; 13: int howManyPositions; 14: }; 15: 16: void Game::Score(int correct, int position) 17: { 18: cout << "\nBeginning score. Correct: "; 19: cout << correct << " Position: " << position << endl; 20: correct = 5; 21: position = 7; 22: cout << "Departing score. Correct: "; 23: cout << correct << " Position: " << position << endl; 24: } 25: 26: void Game::Play() 27: { 28: int correct = 0; 29: int position = 0; 30: 31: cout << "Beginning Play. Correct: "; 32: cout << correct << " Position: " << position << endl; 33: correct = 2; 34: position = 4; 35: cout << "Play updated values. Correct: " ; 36: cout << correct << " Position: " << position << endl; 37: cout << "\nCalling score..." << endl; 38: Score(correct, position); 39: cout << "\nBack from Score() in Play. Correct: "; 40: cout << correct << " Position: " << position << endl; 41: } 42: 43: int main() 44: { 45: 46: Game theGame; 47: theGame.Play(); 48: return 0; 49: } 50: Beginning Play. Correct: 0 Position: 0 51: Play updated values. Correct: 2 Position: 4 52: 53: Calling score... 54: 55: Beginning score. Correct: 2 Position: 4 56: Departing score. Correct: 5 Position: 7 57: 58: Back from Score() in Play. Correct: 2 Position: 4
The very first thing to note is that I've moved everything into one file: Decryptix.cpp. This is for convenience only. In a real program, the declaration of Game would be in Game.h, the implementation of Game would be in Game.cpp, and so forth.
Let's examine the code. On line 6, you see that we've simplified the constructor to take no action, and we've implemented it inline. For the purpose of this illustration, we don't need to focus on anything except the invocation of Score() from Play(). On line 9, you might notice that I've simplified the signature of Score():, eliminating the array of characters. We'll come back to how to pass an array into a function later, but for now I want to focus on the two integer variables, correct and position. Note that in this illustration the ampersand (&) is gone: We're now passing by value, not by reference.
Program flow begins in main(), toward the bottom of the file on line 43. On line 46 we create an instance of a Game object, and at (16) we invoke (or call) the method Play() on that method.
This call to Play() causes program flow to jump to the beginning of Play() on line 26. We start by initializing both correct and position to 0, on line 28. We then print these values on line 32, which is reflected in the output on line 50.
Next, on lines 33 and 34 we change the values of correct and position to 2 and 4, respectively, and then on line 36 we print them again, which is shown in the output on line 51.
On line 38 we invoke Score(), passing in correct and position. This causes the program flow to jump to the implementation of Score(), which is shown on lines 16-24.
The signature of Score() at its implementation matches that of Score() in the class declaration, as it must. Thus, correct and position are passed in by value. This is exactly as if you had declared local variables in this function and initialized them to the values they had in Play().
On line 19 we print correct and position and, as the output shows on line 55, they match the values they had in Play().
On lines 20 and 21, we change these values to 5 and 7, and then on line 23 we print them again to prove that the change occurred; this appears in the output at line 56.
Score() now returns, and program flow resumes on 39; the values are printed again, as shown in the output on line 58.
Until this moment, everything has proceeded according to plan; however, the values back in Play() are not changed, even though you know they were in Score(). Step through this in your debugger, and you'll find that the values are changed in Score(), but when you are back in Play(), they are unchanged.
As you have probably already guessed, this is the result of passing the parameters by value. If you make one tiny change to this program and declare the values to be passed by reference, this program works as expected (see Listing 5.4).
0: #include <iostream> 1: using namespace std; 2: 3: class Game 4: { 5: public: 6: Game(){} 7: ~Game(){} 8: void Play(); 9: void Score(int & correct, int & position); 10: 11: private: 12: int howManyLetters; 13: int howManyPositions; 14: }; 15: 16: void Game::Score(int & rCorrect, int & rPosition) 17: { 18: cout << "\nBeginning score. Correct: " << rCorrect 18a: << " Position: " << rPosition << endl; 19: rCorrect = 5; 20: rPosition = 7; 21: cout << "Departing score. Correct: "; << rCorrect; 21a: cout << " Position: " << rPosition << endl; 22: } 23: 24: void Game::Play() 25: { 26: int correct = 0; 27: int position = 0; 28: 29: cout << "Beginning Play. Correct: " << correct; 29a: cout << " Position: " << position << endl; 30: correct = 2; 31: position = 4; 32: cout << "Play updated values. Correct: " << correct; 32a: cout << " Position: " << position << endl; 33: cout << "\nCalling score..." << endl; 34: Score(correct, position); 35: cout << "\nBack from Score() in Play. Correct: " << correct; 35a: cout << " Position: " << position << endl; 36: } 37: 38: int main() 39: { 40: 41: Game theGame; 42: theGame.Play(); 43: return 0; 44: } 45: Beginning Play. Correct: 0 Position: 0 46: Play updated values. Correct: 2 Position: 4 47: 48: Calling score... 49: 50: Beginning score. Correct: 2 Position: 4 51: Departing score. Correct: 5 Position: 7 52: 53: Back from Score() in Play. Correct: 5 Position: 7
The only change in this version is to the signature of Score() (on line 9), which is matched in the implementation (on line 16). The parameter names (for example, rCorrect) need not match between the declaration and the implementation.
NOTE: The parameter names are actually optional at the declaration. If you leave them off, the program compiles without error. As a general programming practice, however, be sure to include good parameter names even though they are not required. They serve as documentation and make your source code easier to understand.
The invocation of Score() on line 34 does not change at all. The client of Score() doesn't have to manage the fact that you are now passing correct and position by reference.
The output illustrates on line 53 that the change to the values in Score() did change the values back in Play(). This happens because this time no copy was made--you were changing the actual values.
The change in the signature is a change in type. You have changed correct from the integer
int correct
into a reference to an integer:
int & rCorrect
A reference is a special type that acts as an alias.
NOTE: A reference is a type that acts as an alias to an existing object.
The references rCorrect and rPosition are used within Score() exactly as if they were normal integer variables, but the values assigned to them are actually assigned to the original variables--correct and position--back in Play().
NOTE: The name I've given it, rCorrect, is a clue to me that this is a reference. I tend to prepend reference variables with the letter r and pointers (discussed later) with the letter p, but the language does certainly not require this. You can name the variables in Score() and the variables in Play() using exactly the same names, but it makes the source code a bit more difficult to understand.
prepend--Programmers use the term prepend to indicate that you add something to the beginning of a term or variable. Thus, we prepend the letter p to variable names for pointers. The Free Online Dictionary of Computing (http://www.instantweb.com/foldoc/foldoc.cgi?query=prepend) defines prepend as follows: /pree pend/ (by analogy with append) To prefix or add to the beginning.
It is important to distinguish between passing by reference and passing a reference. There are two ways to pass by reference. So far we've examined one way: using a reference. Let's take a look at the alternative--pointers.
Your throat tightens, your pulse quickens, and a cold, sickening dread grows in the pit of your stomach. Nothing unnerves new C++ programmers as does working with pointers. Well, relax. When you understand that a pointer is nothing more than a variable that holds the address in memory of another variable, pointers are a piece of cake.
When you create objects in your program, you create them in memory.
When you create the local variable correct in the method Play(), correct is kept in memory. Later you'll examine where in memory variables live, but for now it doesn't matter. What does matter is that correct is in memory, and every location in memory has an address. Normally, you don't care about the specific address of an object because you have a label (or name), for example, correct. If you need to get to correct, you can do so with its label.
You can get the address of correct by using the address of operator (&):
&correct;
NOTE: The address-of operator (&) uses the same ampersand that we used to identify references. The compiler can tell which you want by context.
When you have the address of correct, you can stash that address in a pointer. A pointer is a variable that holds the address of some object in memory.
pointer--A variable that holds the address of an object.
You can imagine that each memory location is ordered sequentially, as an offset from some arbitrary starting point. Picture, if you will, a series of cubbyholes, all aligned and numbered, perhaps as shown in Figure 5.2.
Figure 5.2 Memory as cubbyholes.
Every integer, character, or object you create is stored in these addresses. Because each cubby holds one byte, a 4-byte integer such as correct takes up four such locations.
correct's address is just the first byte of that storage location. Because the compiler knows that an integer is 4 bytes, if its address is 0x001, the compiler knows it occupies 0x001, 0x002, 0x003, and 0x004, and therefore puts the next integer at 0x005-0x008.
NOTE: These addresses are in hexadecimal, which is a base-16 numbering system. If you are curious about this, please see Appendix A, "Binary and Hexadecimal."
There are two perspectives on what is stored in these locations. One is the bit perspective, which is pretty close to how the compiler "thinks" about memory (see Figure 5.3).
Figure 5.3 How the compiler thinks about data in memory.
From this perspective, the four bytes are filled with binary digits. How these values are interpreted is irrelevant to the compiler. The bits might represent an integer, a character, or an address somewhere else in memory. We'll return to that idea in a moment.
The point is that the compiler doesn't know or care how to interpret the bits--it just knows what is stored at a given location. To the programmer, however, this memory is conceived somewhat differently (as shown in Figure 5.4).
Figure 5.4 How programmers think about data in memory.
To the programmer, the value 5 is stored at this location like a letter in a mailbox. The programmer doesn't much care how the bits are configured, he just knows that the value is stashed away at a particular location.
Let's return to the idea that you can store a memory address. This is a powerful idea. We show here that the value 5 is stored at memory location 0x001. What if you take that address, 0x001 (which in binary is 00000000 00000000 00000000 00000001), and you stash that pattern at another address in memory: 0x1101 (see Figure 5.5).
Figure 5.5 Storing the address.
NOTE: There are some simplifying assumptions here that do not distort the point of this discussion. For example, these are not real memory locations, and values are often stored in memory in a slightly different order than is shown. In addition, compilers often store values at even boundaries in memory.
Here you see that at location 1101, you have stored the value 0x001: the memory location at which you stored the value 5.
At that pointed to address, you hold the value 5 as illustrated in Figure 5.4. You can now assign this address to a variable that holds an address--a pointer. You declare a pointer by indicating the type of object it points to (in this case, int), followed by the pointer operator (*), followed by the name of the variable:
int * pCorrect;
This declares pCorrect to be a pointer to an integer. You can then assign the address of any integer, in this case correct, to that pointer:
pCorrect = &correct;
Thus, pCorrect now contains the address of the score, as shown in Figure 5.6.
Figure 5.6 pCorrect points to correct.
pCorrect is a pointer to an integer. The integer itself, correct, is stored at 0x001, and pCorrect stores the address of that integer.
The pointer does not have to be in the same method as the variable. In fact, by passing the address into a method and manipulating it with a pointer, you can achieve the same pass by reference effect you achieved using references. Listing 5.5 illustrates this point by rewriting Listing 5.4 using pointers.
0: #include <iostream> 1: using namespace std; 2: 3: class Game 4: { 5: public: 6: Game(){} 7: ~Game(){} 8: void Play(); 9: void Score(int * correct, int * position); 10: 11: private: 12: int howManyLetters; 13: int howManyPositions; 14: }; 15: 16: void Game::Score(int * pCorrect, int * pPosition) 17: { 18: cout << "\nBeginning score. Correct: " << * pCorrect 18a: << " Position: " << * pPosition << endl; 19: * pCorrect = 5; 20: * pPosition = 7; 21: cout << "Departing score. Correct: " << * pCorrect 21a: << " Position: " << * pPosition << endl; 22: } 23: 24: void Game::Play() 25: { 26: int correct = 0; 27: int position = 0; 28: 29: cout << "Beginning Play. Correct: " << correct 29a: << " Position: " << position << endl; 30: correct = 2; 31: position = 4; 32: cout << "Play updated values. Correct: " << correct 32a: << " Position: " << position << endl; 33: cout << "\nCalling score..." << endl; 34: Score(&correct, &position); 35: cout << "\nBack from Score() in Play. Correct: " << correct 35a: << " Position: " << position << endl; 36: } 37: 38: int main() 39: { 40: 41: Game theGame; 42: theGame.Play(); 43: return 0; 44: } 45: Beginning Play. Correct: 0 Position: 0 46: Play updated values. Correct: 2 Position: 4 47: 48: Calling score... 49: 50: Beginning score. Correct: 2 Position: 4 51: Departing score. Correct: 5 Position: 7 52: 53: Back from Score() in Play. Correct: 5 Position: 7
The signature to Score() has changed again, as shown on lines 9 and 16. This time, pCorrect and pPosition are declared to be pointers to int: They hold the address of an integer.
On line 34, Play() calls Score() and passes in the addresses of correct and position using the address-of operator (&). There is no reason to declare a pointer here. All you need is the address, and you can get that using the address-of operator.
The compiler puts this address into the pointers that are declared to be the parameters to Score(). Thus, on line 34, the variables pCorrect and pPosition are filled with the addresses of correct and position, respectively.
On line 18 you want to print the values of correct and position. You don't want the values of pCorrect and pPosition because these are addresses. Rather, you want to print the values at the variables whose addresses these pointers hold.
Similarly, on line 19 and 20 you want to set a new value into the variable whose address is stored in pCorrect. You do not want to write
pCorrect = 5;
because that assigns 5 to pCorrect, and pCorrect needs an address, not a simple integer value.
NOTE: This will compile, but it stores the address 5 to this pointer, which is a disaster waiting to happen.
The dereference operator (*) is used. Again, this is the pointer operator, but its meaning is understood in context.
dereference operator--The dereference operator is used to access the object to which the pointer points.
The dereference operator returns the object whose address is stored in the pointer. Thus
*pCorrect
returns the variable correct. By writing
*pCorrect = 5;
we store the value 5 in correct.
NOTE: I read the statement
*pCorrect = 5
NOTE: as "set the value at pCorrect to 5." That is, assign 5 to the variable whose address is stored in pCorrect.
There are two hurdles for a novice programmer--syntax and semantics--and pointers challenge you on both. The syntax is different because the same symbols (& and *) are used for many different purposes. The asterisk is used for multiplication, for the declaration of a pointer, and for dereferencing:
z = x * y; // z equals x multiplied by y int * ptr; // declare a pointer
*ptr = 7; // assign 7 to the dereferenced pointerSimilarly, the ampersand is used for references, for the address-of operator, and for logical AND:
if ( x && y ) // if x and also y ptr = &x; // address-of operator
int & x = y; // initialize a referenceMore important than the confusing syntax is the difficulty with semantics. When I assign the value 5 on line 19, realize that I'm assigning 5 to correct in Play() indirectly through the pointer that is passed into Score().
Listing 5.6 reproduces the excerpt of Play() that we were examining in Listing 5.2 when we went off on the discussion of pointers. Remember that on line 10 we call Score() with three parameters, the first of which is our array of characters--guess. We've considered the two other parameters, correct and position, which are passed by reference using references. How is guess passed?
0: void Game::Play() 1: { 2: char guess[80]; 3: int correct = 0; 4: int position = 0; 5: 6: //... 7: cout << "\nYour guess: "; 8: Display(guess); 9: 10: Score(guess,correct,position); 11: cout << "\t\t" << correct << " correct, " << position 11a: << " in position." << endl; 12: }
You must always pass arrays by reference; this is fairly easy to accomplish because C++ supports a close symmetry between arrays and pointers.
Every nonstatic member method has, as a hidden parameter, a pointer called the this pointer. The this pointer has the address of the object itself. When you write
const char * Game::GetSolution() { return solution; }
the compiler invisibly turns it into
const char * Game::GetSolution(Game * this) { return this->solution; }
You are free to use the this pointer explicitly: you can write your code as follows
const char * Game::GetSolution() { return this->solution; }
but there is little point in doing so. That said, there are times when you will use the this pointer explicitly to obtain the address of the object itself. We'll discuss this later in the book.
When you declare the member method const, the compiler changes the this pointer from a pointer to an object into a pointer to a constant object. Thus, the compiler turns the following code
const char * Game::GetSolution() const { return solution; }
into
const char * Game::GetSolution(const Game * this) const { return this->solution; }
The constant this pointer enforces the constancy of class method.
The name of an array (in our case, guess) is thought of as a pointer to the first element in the array. You can access elements of an array using the offset operator ([]), or by using the name of the array and what is called pointer arithmetic. Listing 5.7 illustrates this relationship between pointers and arrays.
pointer arithmetic--You can determine how many objects are in a range by subtracting the address of one pointer from another.
0: #include <iostream> 1: using namespace std; 2: 3: int main() 4: { 5: char myString[80]; 6: strcpy(myString,"Hello there"); 7: cout << "myString is " << strlen(myString) 7a: << " characters long!" << endl; 8: cout << "myString: " << myString << endl; 9: char c1 = myString[1]; 10: char c2 = *(myString + 1); 11: cout << "c1: " << c1 << " c2: " << c2 << endl; 12: char * p1 = myString; 13: char * p2 = myString + 1; 14: cout << "p1: " << p1 << endl; 15: cout << "p2: " << p2 << endl; 16: cout << "myString+1: " << myString+1 << endl; 17: myString[4] = 'a'; 18: cout << "myString: " << myString << endl; 19: *(myString+4) = 'b'; 20: cout << "myString: " << myString << endl; 21: p1[4] = 'c'; 22: cout << "myString: " << myString << endl; 23: *(p1+4) = 'd'; 24: cout << "myString: " << myString << endl; 25: myString[4] = 'o'; 26: myString[5] = '\0'; 27: cout << "myString: " << myString << endl; 28: return 0; 29: } 30: myString is 11 characters long! 31: myString: Hello there 32: c1: e c2: e 33: p1: Hello there 34: p2: ello there 35: myString+1: ello there 36: myString: Hella there 37: myString: Hellb there 38: myString: Hellc there 39: myString: Helld there 40: myString: Hello
On line 5, we create a character array that is large enough to hold the string. On line 6 we use the old-fashioned C-style string library routine strcpy to copy into our array a null-terminated string with the words Hello there.
On line 7 we use the C-style library routine strlen to obtain the length of the string. This measures the number of characters until the first NULL and returns that value (11) as an integer, which is printed by cout and shown on line 30.
On line 8 we pass the array to cout, which treats the name of the array (myString) as a pointer to the first byte of the string. cout knows that when it is given an array name it is to print every character until the first NULL. This is shown on line 30.
On line 9 we create a character variable, c1, which is initialized with the character at offset 1--that is, the second character in the array, e.
On line 13 we treat myString as a pointer and add one to it. When you add one to a pointer, the compiler looks at the type of the object that is pointed to, which in this case is char. It uses the type to determine the size of the object, which in this case is one byte. It then returns the address of the next object of that size. If this were a pointer to int, it would return the address of the next int, four bytes later in memory.
Take the address that is returned (myString+1) and dereference it; this returns the character at that address. Then initialize a new character variable, c2, with that character. Note that c2 is not a pointer; by dereferencing, you're actually getting a character, and that is what is assigned to c2.
We print these two characters on line 11, and the printout is on line 32.
On line 12 we create a pointer to a character and assign it to myString. Because the name of the array acts as a pointer to the first byte of the array, p1 now also points to the first byte. On line 13 we create a second pointer and point it to the second character in the array. These are printed on lines 14 and 15 and shown at lines 33 and 34). This illustrates that in each case cout acts as expected, printing the string beginning at the byte that is pointed to and continuing until the first NULL. These have the same printout as on line 18, which uses the string offset directly and which is shown on line 35.
On line 21 we use the offset operator to change the character that is stored at offset 4 (the fifth character). This is printed, and the output appears on line 36.
You can accomplish the same thing on line 16 by using pointer arithmetic and dereferencing the address that is returned; see the output on line 37. Because p1 is pointing to myString, you can use the offset operator on the pointer on line 21. Remember that the name of the array is a pointer to the first element--and that is exactly what p1 is. The effect on line 38 is identical.
Similarly, on line 23 we can use pointer arithmetic on p1 and then dereference the resulting address, just as we did with the array name. The resulting printout is shown on line 39.
On line 25 we change the value back to 'o' using the offset operator, and then we insert a null at offset 5. You can do the same thing with pointer arithmetic, but you get the point. As you probably remember, cout prints only to the first null, so the string hello is printed; nothing further in the array is printed, however, even though the word there still remains. This is shown on line 40.
We said earlier that guess is passed by reference, as a pointer. What you see passed in Listing 5.6 is the name of the array, which is a pointer to the first element in the array:
Score(guess,correct,position);In Score() this first parameter must be declared as a pointer to character, which it is. Listing 5.8 reproduces Listing 5.1, the declaration of the Game class.
0: #ifndef GAME_H 1: #define GAME_H 2: 3: #include "definedValues.h" 4: 5: class Game 6: { 7: public: 8: Game(); 9: ~Game(){} 10: void Display(const char * charArray)const{ cout << charArray << 10a: endl;} 11: void Play(); 12: const char * GetSolution() const { return solution; } 13: void Score(const char * thisGuess, int & correct, int & position); 14: 15: private: 16: int howMany(const char *, char); 17: char solution[maxPos]; 18: int howManyLetters; 19: int howManyPositions; 20: int round; 21: bool duplicates; 22: }; 23: 24: #endif
You can see on line 13 that the first parameter to Score() is declared as a pointer to char, just as we require. Listing 5.9 shows the implementation of the Score() method.
0:void Game::Score(const char * thisGuess, int & correct, int & position) 1: { 2: correct = 0; 3: position = 0; 4: 5: for ( int i = 0; i < howManyLetters; i++) 6: { 7: int howManyInGuess = howMany (thisGuess, alpha[i]); 8: int howManyInAnswer = howMany (solution, alpha[i]); 9: correct += howManyInGuess < howManyInAnswer ? 10: howManyInGuess : howManyInAnswer; 11: 12: } 13: 14: for ( int j = 0; j < howManyPositions; j++) 15: { 16: if ( thisGuess[j] == solution[j] ) 17: position++; 18: } 19: 20: }
The signature on the implementation agrees, as it must, with the declaration. thisGuess is a pointer to char and is the same array as guess in Play(). Because guess was passed by reference (as arrays must be), this is the same array, and changes to this array are reflected back in Play.
Because you must pass by reference but you do not want to allow Score() to change this array (and there is no reason for it to do so), declare the parameter to be a pointer to a constant char rather than a pointer to char. This keyword const says to the compiler, "I don't intend to change the object that is pointed to, so tell me if I do." This way, the compiler taps you on the shoulder if you attempt to make such a change and says, "Excuse me, sir, but you've changed an object when you promised you wouldn't. Not cricket, sir." (Your compiler's error message might vary).
Let's walk through this implementation of Score() line by line. On lines 2 and 3, we initialize both integers, correct and position, to 0. If we take no other action, the score is zero correct and zero in position.
On line 5 we begin a for loop that will run once for each letter in thisGuess. The body of the for loop consists of three statements.
On line 7 a local variable--howManyInGuess--is initialized to store the result of calling the private member method howMany(). When we call howMany, we pass in the pointer to the array as the first parameter and the letter at alpha[i] as the second parameter.
This is a classic C++ statement, which does at least three things at once. Let's take the statement apart.
The first thing that happens is that alpha[i] is returned. The first time through this loop, alpha[0] is returned, which is 'a'. The second time through, 'b' is returned, and so forth.
This letter becomes the second parameter to the call to howMany(). If you look back at the declaration of Game, you'll find that howMany() is a private method that takes two parameters: a pointer to a constant char (the guess from Play()) and a character. Listing 5.10 shows the implementation of howMany().
0: inline int Game::howMany(const char * theString, char c) 1: { 2: int count = 0; 3: for ( int i = 0; i < strlen(theString); i++) 4: { 5: if ( theString[i] == c ) 6: count ++; 7: } 8: return count;
9: }The purpose of this method is to return the number of times an individual letter occurs in an array of characters. On line 2 the counter is initialized to zero. On line 3 we begin a for loop that iterates through every position in the array.
On line 5 we test each character to see whether it matches the character that was sent in to be tested; if so, we increment the counter. Note that the braces at lines 4 and 7 are not technically necessary, but as Donald Xie pointed out when editing this book, they do make the code much easier to read.
Finally, on line 8 we return that value.
In Listing 5.9, on line 7, we now have a value on the right side of the assignment that represents how many times alpha[i] occurs in thisGuess: that is, in the array that is passed in from Play().
On line 8, we compute the same value for the solution. The value of correct is the lesser of these two, which we accomplish on lines 9 and 10 by using the ternary operator to find the smaller value.
An example makes this clearer: If the solution has aabba and the guess has ababc, we examine the first letter a. howMany() returns 2 for the guess and 3 for the solution, so the player has the lesser, 2, correct.
On lines 14-18, we iterate again through the loops, this time testing on line 16 to see whether the character at a specific offset in thisGuess is the same as the character at the same offset in the solution. If so, another letter is in the right position.
Because correct and position were passed in as references, the changes that are made in Score() are reflected back in Play().
Before moving on, I want to demonstrate how this code can be made both more reliable and more understandable through the use of ASSERT.
The purpose of ASSERT is to test your assumptions when you are debugging your code, but to have no effect at all when you release your final production version.
When you are debugging your code, you signal your compiler to enter debug mode. When you are ready to release your program to the paying public, you rebuild in release mode. Debug mode brings along a lot of debugging information that you don't want in release mode.
Thus, in debug mode, you can write
ASSERT ( position <= correct )
Here you are simultaneously documenting your belief that position must never be larger than correct. (You can never have five in the correct position if you only have four correct letters!) You are also testing that assertion each time the code runs to prove that you are right. In debug mode, if position ever is larger than correct, this ASSERT statement fails and an error message is written.
When your program is ready to be released, the ASSERT macro magically disappears and has no effect on the efficiency of your code.
ASSERT is typically implemented as a macro. Macros are left over from C; they are type-unsafe routines that are processed not by the compiler but by the precompiler, the same beast that handles your #include and #define statements. In fact, a macro is a #define statement.
macro--A text substitution by the precompiler. Macros can act as small subprograms.
A macro function is a symbol that is created using #define, which takes an argument much like a function does, and which replaces the macro and its argument with a substitution string. For example, you can define the macro TWICE as follows:
#define TWICE(x) ( (x) * 2 )
Then in your code you write
TWICE(4)
The entire string TWICE(4) is removed and the value 4*2 is substituted. When the precompiler sees TWICE(4), it substitutes ( (4) * 2 ). That is just what you want because 4*2 evaluates to 8, so TWICE will have done just the work you expected.
A macro can have more than one parameter, and each parameter can be used repeatedly in the replacement text. Two common macros are MAX and MIN:
#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) #define MIN(x,y) ( (x) < (y) ? (x) : (y) )
MAX returns the larger of two values (x and y), and MIN returns the lesser. Thus, MAX(7,5) is 7, and MIN(7,5) is 5.
NOTE: In a macro function definition, the opening parenthesis for the parameter list must immediately follow the macro name, with no spaces. The preprocessor is not as forgiving of white space as is the compiler.
You might be wondering why there are so many parentheses in these macros. The preprocessor does not demand that parentheses be placed around the arguments in the substitution string, but the parentheses help you avoid unwanted side effects when you pass complicated values to a macro. For example, if you define MAX as
#define MAX(x,y) x > y ? x : y
and pass in the values 5 and 7, the macro works as intended. If you pass in a more complicated expression, however, you'll get unintended results, as shown in Listing 5.11.
0: 1: #include <iostream.h> 2: 3: #define CUBE(a) ( (a) * (a) * (a) ) 4: #define THREE(a) a * a * a 5: 6: int main() 7: { 8: long x = 5; 9: long y = CUBE(x); 10: long z = THREE(x); 11: 12: cout << "y: " << y << endl; 13: cout << "z: " << z << endl; 14: 15: long a = 5, b = 7; 16: y = CUBE(a+b); 17: z = THREE(a+b); 18: 19: cout << "y: " << y << endl; 20: cout << "z: " << z << endl; 21: return 0; 22: } ***Please Insert Output icon herey: 125 z: 125 y: 1728 z: 82
On line 1, we use the old-fashioned iostream.h so that we can avoid using namespaces. This is perfectly legal in C++, and it is common in writing very short demonstration programs.
On line 3, the macro CUBE is defined, with the argument x put into parentheses each time it is used. On line 4, the macro THREE is defined, without the parentheses. It is intended for these macros to do exactly the same thing: to multiply their arguments times themselves, three times.
In the first use of these macros, on line 16, the value 5 is given as the parameter and both macros work fine. CUBE(5) expands to ( (5) * (5) * (5) ), which evaluates to 125, and THREE(5) expands to 5 * 5 * 5, which also evaluates to 125.
In the second use, on line 17, the parameter is 5 + 7. In this case, CUBE(5+7) evaluates to
( (5+7) * (5+7) * (5+7) )
which evaluates to
( (12) * (12) * (12) )
which in turn evaluates to 1,728. THREE(5+7), however, evaluates to
5 + 7 * 5 + 7 * 5 + 7
Because multiplication has a higher precedence than addition, this becomes
5 + (7 * 5) + (7 * 5) + 7
which evaluates to
5 + (35) + (35) + 7
which finally evaluates to 82.
Macros suffer from four problems in the eyes of a C++ programmer. First, because all macros must be defined on one line, they can be confusing if they become large. You can extend that line by using the backslash character (\), but large macros quickly become difficult to manage.
Second, macros are expanded inline each time they are used. This means that if a macro is used a dozen times, the substitution appears 12 times in your program, rather than appearing once as a function call does. On the other hand, they are usually quicker than a function call because the overhead of a function call is avoided.
The fact that they are expanded inline leads to the third problem, which is that the macro does not appear in the intermediate source code that is used by the compiler, and therefore it is not visible in most debuggers. By the time you see it in the debugger, the substitution is already accomplished. This makes debugging macros tricky.
The final problem, however, is the largest: Macros are not type-safe. Although it is convenient that absolutely any argument can be used with a macro, this completely undermines the strong typing of C++ and so is anathema to C++ programmers.
That said, the ASSERT macro is a good example of a time when this is not a bug, but a feature: One ASSERT macro can test any condition, mathematical or otherwise.
The preprocessor provides two special operators for manipulating strings in macros. The stringizing operator (#) substitutes a quoted string for whatever follows the stringizing operator. The concatenation operator (##) bonds two strings together into one.
NOTE: The stringizing operator (#) substitutes a quoted string for whatever follows the stringizing operator.
The concatenation operator (##) bonds two strings together into one.
The stringizing operator(#) puts quotes around any characters that follow the operator, up to the next white space. Thus, if you write
#define WRITESTRING(x) cout << #x
and then call
WRITESTRING(This is a string);
the precompiler turns it into
cout << "This is a string";
Note that the string This is a string is put into quotes, as is required by cout.
The concatenation operator (##) enables you to bond together more than one term into a new word. The new word is actually a token that can be used as a class name, a variable name, or an offset into an array--or anywhere else a series of letters might appear.
Assume for a moment that you have five functions named fOnePrint, fTwoPrint, fThreePrint, fFourPrint, and fFivePrint. You can then declare
#define fPRINT(x) f ## x ## Print
and then use it with fPRINT(Two) to generate fTwoPrint, and with fPRINT(Three) to generate fThreePrint.
Many compilers predefine a number of useful macros, including __DATE__, __TIME__, __LINE__, and __FILE__. Each of these names is surrounded by two underscore characters to reduce the likelihood that the names will conflict with names you've used in your program.
When the precompiler sees one of these macros, it makes the appropriate substitutes. For __DATE__, the current Date is substituted; for __TIME__, the current time is substituted. __LINE__ and __FILE__ are replaced with the source code line number and filename, respectively. Note that this substitution is made when the source is precompiled, not when the program is run. If you ask the program to print __DATE__, you do not get the current date; instead, you get the date the program was compiled. These defined macros are very useful in debugging.
Although many compilers do provide an ASSERT macro, it will be instructive to create our own, shown in Listing 5.12.
0: #define DEBUG 1: 2: #ifndef DEBUG 3: #define ASSERT(x) 4: #else 5: #define ASSERT(x) \ 6: if (! (x)) \ 7: { \ 8: cout << "ERROR!! Assert " << #x << " failed\n"; \ 9: cout << " on line " << __LINE__ << "\n"; \ 10: cout << " in file " << __FILE__ << "\n"; \ 11: } 12: #endif
On line 0, we define the value DEBUG, which we test on line 2. In the production version we'll remove the definition of DEBUG, and the test on line 2 will fail. When the test fails, this macro defines ASSERT(x) to do nothing, as shown on line 3. If the test succeeds, as it will while we are debugging, this macro defines ASSERT as shown on line 5.
In a macro, any line ending with \ continues on the next line as if both were on the same line. The entire set of lines from line 5 to line 11 is thus considered a single line of the macro. On line 6, whatever is passed to the macro (x) is tested; if it fails, the body of the if statement executes, writing an error message to the screen.
On line 8, we see the stringizing macro at work, and the following lines take advantage of the __FILE__ and __LINE__ macros that are supplied by the compiler vendor.
I don't show ASSERT macros everywhere they might appear in this book because they can detract from the point that is being made; at other times, however, they can greatly clarify the program. For example, I'd rewrite Score() as shown in Listing 5.13.
0:void Game::Score(const char * thisGuess, int & correct, int & position) 1: { 2: correct = 0; 3: position = 0; 4: 5: ASSERT ( strlen(thisGuess) == howManyPositions ) 6: ASSERT ( strlen(solution) == howManyPositions ) 7: 8: for ( int i = 0; i < howManyLetters; i++) 9: { 10: int howManyInGuess = howMany (thisGuess, alpha[i]); 11: int howManyInAnswer = howMany (solution, alpha[i]); 12: correct += howManyInGuess < howManyInAnswer ? 12a: howManyInGuess : howManyInAnswer; 13: } 14: 15: for ( i = 0; i < howManyPositions; i++) 16: { 17: if ( thisGuess[i] == solution[i] ) 18: position++; 19: } 20: 21: ASSERT ( position <= correct ) 22: 23: }
The ASSERT on line 5 documents and tests my assumption that the string passed in as thisGuess is exactly howManyPositions long. The ASSERT on line 6 does the same for the solution. Finally, the ASSERT on line 21 documents and tests my assumption that the number in the correct position can never be greater than the number of correct letters.
Listing 5.14 provides the complete listing of this program. Let's walk through one round, line by line.
1: #ifndef DEFINED 2: #define DEFINED 3: 4: #include <iostream> 5: using namespace std; 6: 7: const char alpha[] = "abcdefghijklmnopqrstuvwxyz"; 8: const int minPos = 2; 9: const int maxPos = 10; 10: const int minLetters = 2; 11: const int maxLetters = 26; 12: 13: #define DEBUG 14: 15: #ifndef DEBUG 16: #define ASSERT(x) 17: #else 18: #define ASSERT(x) \ 19: if (! (x)) \ 20: { \ 21: cout << "ERROR!! Assert " << #x << " failed\n"; \ 22: cout << " on line " << __LINE__ << "\n"; \ 23: cout << " in file " << __FILE__ << "\n"; \ 24: } 25: #endif 26: 27: #endif
28: #ifndef GAME_H 29: #define GAME_H 30: 31: #include "DefinedValues.h" 32: 33: class Game 34: { 35: public: 36: Game(); 37: ~Game(){} 38: void Display(const char * charArray) const 39: { 40: cout << charArray << endl; 41: } 42: void Play(); 43: const char * GetSolution() const { return solution; } 44: void Score(const char * thisGuess, int & correct, int & position); 45: 46: private: 47: int HowMany(const char *, char); 48: char solution[maxPos+1]; 49: int howManyLetters; 50: int howManyPositions; 51: int round; 52: bool duplicates; 53: }; 54: 55: #endif
56: #include <time.h> 57: #include "Game.h" 58: 59: void Game::Score(const char * thisGuess, int & > rCorrect, int & > rPosition) 60: { 61: rCorrect = 0; 62: rPosition = 0; 63: 64: ASSERT ( strlen(thisGuess) == howManyPositions) 65: ASSERT ( strlen(solution) == howManyPositions) 66: int i; 67: for ( i = 0; i < howManyLetters; i++) 68: { 69: int howManyInGuess = HowMany (thisGuess, alpha[i]); 70: int howManyInAnswer = HowMany (solution, alpha[i]); 71: rCorrect += howManyInGuess < howManyInAnswer ? 72: howManyInGuess : howManyInAnswer; 73: } 74: 75: for ( i = 0; i < howManyPositions; i++) 76: { 77: if ( thisGuess[i] == solution[i] ) 78: rPosition ++; 79: } 80: 81: ASSERT ( rPosition <= rCorrect) 82: 83: } 84: 85: Game::Game(): 86: round(1), 87: howManyPositions(0), 88: howManyLetters(0), 89: duplicates(false) 90: { 91: 92: bool valid = false; 93: while ( ! valid ) 94: { 95: while ( howManyLetters < minLetters || 96: howManyLetters > maxLetters ) 97: { 98: cout << "How many letters? ("; 99: cout << minLetters << "-" << maxLetters << "): "; 100: cin >> howManyLetters; 101: if ( howManyLetters < minLetters || 102: howManyLetters > maxLetters ) 103: cout << "please enter a number between "; 104: cout << minLetters << " and " << maxLetters << endl; 105: } 106: 107: while ( howManyPositions < minPos || 107a: howManyPositions > maxPos ) 108: { 109: cout << "How many positions? ("; 110: cout << minPos << "-" << maxPos << "): "; 111: cin >> howManyPositions; 112: if ( howManyPositions < minPos || 112a: howManyPositions > maxPos ) 113: cout << "please enter a number between "; 114: cout << minPos <<" and " << maxPos << endl; 115: } 116: 117: char choice = ' '; 118: while ( choice != 'y' && choice != 'n' ) 119: { 120: cout << "Allow duplicates (y/n)? "; 121: cin >> choice; 122: } 123: 124: duplicates = choice == 'y' ? true : false; 125: 126: if ( ! duplicates && howManyPositions > howManyLetters ) 127: { 128: cout << "I can't put " << howManyLetters; 128a: cout << " letters in "; 129: cout << howManyPositions; 130: cout << " positions without duplicates! "; 131: cout << Please try again.\n"; 132: howManyLetters = 0; 133: howManyPositions = 0; 134: } 135: else 136: valid = true; 137: } 138: 139: int i; 140: for (i = 0; i < maxPos; i++ ) 141: solution[i] = 0; 142: 143: srand( (unsigned)time( NULL ) ); 144: 145: for ( i = 0; i < howManyPositions; ) 146: { 147: int nextValue = rand() % (howManyLetters); 148: char c = alpha[nextValue]; 149: if ( ! duplicates && i > 0 ) 150: { 151: int count = HowMany(solution, c); 152: if ( count > 0 ) 153: continue; 154: } 155: // add to the array 156: solution[i] = c; 157: i++; 158: } 159: solution[i] = '\0'; 160: 161: } 162: 163: void Game::Play() 164: { 165: char guess[80]; 166: int correct = 0; 167: int position = 0; 168: bool quit = false; 169: 170: while ( position < howManyPositions ) 171: { 172: 173: cout << "\nRound " << round << ". Enter "; 174: cout << howManyPositions << " letters between "; 175: cout << alpha[0] << " and "; 175a: cout << alpha[howManyLetters-1] << ": "; 176: 177: cin >> guess; 178: 179: if ( strlen(guess) != howManyPositions ) 180: { 181: cout << "\n ** Please enter exactly "; 182: cout << howManyPositions << " letters. **\n"; 183: continue; 184: } 185: 186: 187: round++; 188: 189: cout << "\nYour guess: "; 190: Display(guess); 191: 192: Score(guess,correct,position); 193: cout << "\t\t" << correct << " correct, "; 194: cout << position << " in position." << endl; 195: } 196: 197: cout << "\n\nCongratulations! It took you "; 198: 199: if ( round <= 6 ) 200: cout << "only "; 201: 202: if ( round-1 == 1 ) 203: cout << "one round!" << endl; 204: else 205: cout << round-1 << " rounds." << endl; 206: } 207: 208: 209: inline int Game::HowMany(const char * theString, char c) 210: { 211: int count = 0; 212: for ( int i = 0; i < strlen(theString); i++) 213: if ( theString[i] == c ) 214: count ++; 215: return count; 216: }
217: #include "DefinedValues.h" 218: #include "Game.h" 219: 220: int main() 221: { 222: cout << "Decryptix. Copyright 1999 Liberty "; 223: cout << "Associates, Inc. Version 0.3\n\n" << endl; 224: bool playAgain = true; 225: 226: while ( playAgain ) 227: { 228: char choice = ' '; 229: Game theGame; 230: theGame.Play(); 231: 232: cout << "\nThe answer: "; 233: theGame.Display(theGame.GetSolution()); 234: cout << "\n\n" << endl; 235: 236: while ( choice != 'y' && choice != 'n' ) 237: { 238: cout << "\nPlay again (y/n): "; 239: cin >> choice; 240: } 241: 242: playAgain = choice == 'y' ? true : false; 243: } 244: 245: return 0; 246: }
We begin by loading this program in the debugger and placing a break point on line 222, as illustrated in Figure 5.7. Your particular debugger might look somewhat different, but the essentials are probably the same. Choose Run to break point (in Microsoft Visual Studio, this is F5).
Figure 5.7 Examining a break point.
By the time the program has stopped at this break point, it has loaded the two header files, definedValues.h and Game.h.
Loading definedValues brings us to line 1, where the inclusion guards are checked and we find that DEFINED has not yet been defined. Thus, the body of definedValues is read, which brings in iostream (line 4) and which declares (line 5) that we are using the standard namespace.
The global constants are defined on line 7, and ASSERT is defined on line 18.
Including Game.h brings us to line 31, where we attempt to include definedValues. This brings us back to line 1, where the inclusion guards protect us by determining that DEFINED has already been defined, so the rest of definedValues.h is ignored. Returning to line 33, we find the declaration of the Game class.
The constructor and destructor are declared lines 36 and 37. Lines 38-41 are the Display routine, which prints to the screen any character array that is passed in. On line 42 is the heart of the class: the Play() method. On line 43 I've added a new method, GetSolution(), which simply returns the solution as a pointer to a constant character--exactly what is needed by Display(). Finally, On line 44) we see the Score() method, which takes an array of characters (passed in by reference as a pointer to a constant character), and two references to integers.
NOTE: Part of the private interface--not exposed to the public but used by the class's methods to aid in implementation--are several state variables (such as howManyLetters on line 49 and howManyPositions on line 50) that indicate which round we're playing (on line 51) and whether we're allowing duplicates (on line 52).
In addition, line 48 shows the array that holds the solution to the game; line 47 shows a helper function, which is used by other methods of this class to determine how many instances of a particular character are found within any array of characters. You'll see how all the methods work as we step through the code.
Our break point on line 222 causes the program to stop before it prints to the screen. See your debugger's documentation for how to step over a function call; in Microsoft's debugger it is F10. Pressing step-over causes the copyright notice to print, and then the Boolean value. playAgain is initialized to true. This is used in the test on line 226, and of course this test passes because the value was just initialized one line earlier.
This brings us into the body of the while statement, where we create an instance of a Game object on line 229. This causes program flow to jump to the constructor of the Game object on line 85. We see that the member variables are initialized, and we enter the while loop on line 93. On lines 95 and 96 we test whether howManyLetters (initialized to 0) is less than minLetters (set in definedValues.h to 2). Because this proves true, the second half of the OR statement (howManyLetters > maxLetters) is not even evaluated; instead, we enter the while loop on line 98.
The user is prompted to enter how many letters he'll be guessing in this instance of the game. We'll choose 4; that value is stored in the member variable howManyLetters on line 100.
On line 101 we test to ensure that we have a legal value; if not, we print a reminder to the player. Program flow loops back up to line 95, where the value is checked; if we have a valid value we proceed on line 107, where the same logic is applied to the number of positions. We'll choose 3.
On line 121 we prompt the user to tell us whether he wants to allow duplicates. Note that this is not robust code: If the user enters Y rather than y (that is, uppercase rather than lowercase), the while statement continues to prompt him until he gets it right. We'll fix that up in the next version. For now, we'll enter n.
On line 124 we test the value that is received; if it is 'y', we set duplicates to true; otherwise, we set it to false. In this case, we set it to false because we've entered 'n'.
Take a look at the member variables, as shown in Figure 5.8.
Figure 5.8 Examining member variables.
Notice, in the variables window in the lower-left corner, that choice has the value 'n', and in the watch window in the lower-right, that you have howManyLetters 4, and howManyLetters 3. Also note, in the variables window, that duplicates is shown as 0. My debugger cannot handle bools, so it shows true as 1 and false as 0. This is legal in C++ (0 does evaluate false and all other integers evaluate true), but it might be better if the debugger showed the actual Boolean value.
NOTE: These images are from the Visual C++ debugger. In other environments you may find a different display, but you should be able to see the same values and information.
On line 126 we test the logic of the user's choices. If he asks for three letters in four positions without duplicates, we point out that this is impossible.
On line 140 we iterate through the entire array, setting every member to zero. It is interesting to put solution into a watch window in the debugger and step through this loop watching as each offset member of the array is changed to zero. Note that this is zero the numeric value, not 0 the character. In C++, 0 is the value of NULL, so this loop sets our character array to contain nothing but NULLs.
At line 143 we use srand to seed the random number generator. You might find it interesting to step into the call to time, but this is not relevant to our discussion here.
On line 145 we begin the work of populating the solution array. First, the local counter variable i is initialized to zero.
NOTE: You might find that many C++ programmers use the variables i, j, k, l, and m as counter variables in for loops, and many can't even tell you why. Why not a? Why not counter?
This is a perfect example of historical anachronisms living on past the time they make any sense. Back in the ancient days of mainframe computing, early versions of FORTRAN (FORmula TRANslator) used only the variables i, j, k, l, and m as legal counting variables. My second computing language was FORTRAN IV, which I learned in high school in 1971. ("You had zeros? We had to use os!") Old habits die hard.
Just as an aside, my first programming language was Monrobot machine language (1s and 0s), which we programmed using paper tape. The computer on which we ran this also had an assembler called QuickComp, which was used by the programming students. To use QuickComp, you had to load a machine language "loader" by running the appropriate tape before running your program. A few of us hacked the QuickComp tape so that on loading it printed go away, I'm sleeping and then shut down the system. In those days, programming, and my sense of humor, were a lot simpler.
On line 147 we examine the result of applying the modulus operator to the result of calling rand() and howManyLetters. If you want to see this at work, rewrite this line as follows:
// int nextValue = rand() % (howManyLetters); int randResult = rand(); int nextValue = randResult % (howManyLetters);
This way you can see the result from rand() (stored in randResult), and then the effect of the modulus operator.
The first time I ran this on my machine, randResult was 17,516. I note that howManyLetters is 4. 17,516 divided by 4 is equal to exactly 4,379. Thus, there is no remainder, so the value that is returned by the modulus operator is 0.
On line 148 the character variable c is set to the letter at offset 0 in alpha ('a').
On line 149 we test the value of duplicates (in this case, false) and whether i is greater than zero. In this case, i is zero, so the if statement is skipped. On line 156 solution[0] is set to a. Then i is incremented to 1 and is compared with howManyPositions at 41. (Notice that we do not do the increment in the body of the for loop.) This is because we only want to increment i if we get to line 156. We'll see the alternative in just a moment.
On line 147 we generate nextValue again. On my computer this generates a randResult of 14846 and a nextValue of 2. Does this make sense? howManyLetters is 4. It turns out that 14,846 divided by 4 is 3,711, with a remainder of 2. (3,711 times 4 is 14,844). Thus the modulus operator returns 2, and the character c is assigned alpha[2] or c.
This time the if statement at line 149 returns true, and we enter the body of the if statement. On line 151 we assign the result of calling HowMany to the variable count, passing in the solution array and the letter c.
Program flow branches to line 209. The array is now represented as a pointer. On line 211 the local variable count is initialized to zero. On line 212 we iterate through the string that is passed in (the solution), and each time through the loop we test whether the value at the current offset is equal to the character that is passed in .
This time, strlen(theString) is 1. You can test this by inserting a line between, rewriting line 212 as follows:
int stringLength = strlen(theString); for ( int i = 0; i < stringLength; i++ )
C++ programmers snicker at this kind of code, with lots of temporary variables, but I've come to believe strongly that this is the right way to do things. By introducing the temporary variable stringLength, you can examine this value in the debugger in a way that is not possible with the more compact version of this code (in which strlen is used in the for loop test).
We see that this first time stringLength is 1, so the for loop runs only once. Because theString[0] is 'a' and our character c is 'c', the if statement fails and count is not incremented. The for loop then ends, and the value 0 is returned. Program flow now resumes at the line immediately following line 151, where the returned value (count) is tested. Because it is not greater than zero, the continue statement does not execute, and program flow continues on line 156 where the character 'c' is added to the array and, once again, i is incremented.
The third time through the for loop at on line 145, my computer generates the value 5,092, which is also exactly divisible by four, returning a nextValue of 0 and a character of 'a'. This time, when we enter howMany, the character matches, so the counter is incremented and the value 1 is returned from howMany. In this case, when the flow resumes at the line just after the call to howMany on line 151, the if statement returns true (1 is greater than 0), so the continue statement executes. This causes the program flow to immediately return to line 145, where we will generate and test a new value.
This is why you don't want to increment i: After all, you have not yet put a value into solution[2]. Thus, i remains at 2, but the call on line 147 generates a different value; this time, the value is 1,369, which sets nextValue to 1 and the character value c to 'b'. Because 'b' does not yet appear in our array, it is added, and i is incremented. i is now 3, the test on line 145 fails (3 is not less than 3), and we fall through to line 159 where solution[3] is set to null.
The result of all this is that solution looks like this:
solution[0]: 'a' solution[1]: 'c' solution[2]: 'b' solution[3]: 0
The constructor now ends, and we resume on line 230 back in main(). This immediately calls Play(), so processing branches to the implementation of Play() on line 163. The local variables correct and position are initialized to 0 (check your local variables window in your debugger), and the user is prompted to enter a guess on line 173. That guess is stored on line 177 in the array you created on line 165.
We'll guess abc. This fails the test on line 179 because the string length of guess is 3, which is equal to howManyPositions and thus fails the test of not being equal. Processing skips the body of the if statement and continues on line 187, where the member variable round is incremented from zero to 1. On line 190 this guess is passed to Display(), where it is shown to the user; then, on line 192, it is passed into Score().
You can step over Display (in Visual C++, press F10) and then into Score (in Visual C++, press F11) to follow those parts of the program that are of immediate interest. Stepping into Score() causes program flow to branch to line 59.
We immediately set the values that are referenced by rCorrect and rPosition to 0. We then assert that our assumptions about the sizes of these arrays are correct. On line 67, we enter a for loop in which we'll iterate through each array, testing every possible letter and creating a count of how many are correct.
The first time in this loop, i is 0 and therefore passes the test of being less than howManyLetters (which is 3). The first call to HowMany() passes in the current guess (abc), and the letter a (alpha[0]) and returns the value 1. The second call passes in the solution (acb) and the character 'a' and also returns 1.
The next line tests whether howManyInGuess (which is 1) is less than howManyInAnswer (also 1). This is false, so it returns the third part of this ternary operator: howManyInAnswer (which, again, is 1). This value of 1 is added to rCorrect, incrementing it from 0 to 1.
We repeat this for all three letters in the two arrays. Next, on line 75, we reset i to 0 and test whether thisGuess[0] is equal to solution[0]. thisGuess[0] is 'a', and solution[0] is also 'a', so rPosition is incremented from 0 to 1. On the second time through the loop, thisGuess[1] is 'b', but solution[1] is 'c', so rPosition is not incremented. On the third time through, thisGuess[2] is 'c' and solution [1] is 'b', so again, rPosition is not incremented.
There is no need to return a value from this method (which is why it is marked void) because the variables rPosition and rCounter are references to the local variables back in Play(). When we return from Score(), these values are printed and we see that correct is 3 and position is 1.
This returns us to the top of the while loop on line 170; position (now 1) is compared with howManyPositions (currently 3). Because it is still smaller, we're not yet done, and we repeat the loop, offering the user a second guess.
This time let's guess acb. The score we receive is three correct and three in position, and this while loop ends. Program flow resumes on line 197, where we print a congratulatory message. Play() then returns, dropping us on line 232 in main(), where the answer is displayed and you are offered (on line 236) the opportunity to play again.
If you decide not to play again, the value 0 is returned to the operating system on line 245 and the program ends.
On line 230 we invoke the Play() method.
This causes program flow to jump to line 163. To see this, step into this method from line 230. Your debugger brings you to line 163. A few local variables are created and initialized, and then on line 170 we check the value of position (which is zero) to see if it is less than howManyPositions.
© Copyright 1999, Macmillan Computer Publishing. All rights reserved.