C++ From Scratch

Contents


4

Creating Classes


In Chapter 3, "Program Flow," you began to put logic into main() to gather the user's preference. In this chapter you'll look at creating classes to do this work.

Why Classes?

Although it is possible--and perhaps tempting--to just flesh out main() with the additional functionality you want to add to this program, it is a very bad idea.

The point of object-oriented programming is to create objects and assign them responsibility for specific aspects of the game. This fosters encapsulation, and with it maintainability and extensibility.

Maintainability means that the programs can be maintained at less expense. Extensibility means that you can add features without breaking the existing code.

As we design and implement classes, I'll discuss design heuristics: guidelines for designing excellent software.


design heuristics--Guidelines for quality in design


The very first--and perhaps most important--object-oriented design heuristic is that each class needs to have a single area of responsibility, and each object needs to collaborate with other objects to accomplish more complicated tasks.

As a rule, C++ programmers tend to keep main() very simple. Its job is only to create the first object and set off the chain of events that lead to these objects accomplishing their assigned tasks.

You'll begin by creating a Game class that is responsible for keeping track of the user's preferences and getting the game underway.

Creating New Types: Class

Although the built-in types are fine for storing values, they are limited in the complexity of the information they can manage.

Built-in types can be combined, however, into user-defined types that can be far more complex.

For example, suppose you want to store the number of letters from which you'll allow the player to choose and the number of positions (for example, choosing among three numbers in two positions, without duplicates, makes the following codes possible: ab, ba, ac, ca, bc, and cb). You can store these two values in variables, or you can store both of them along with the decision as to whether to allow duplicates--all within a Game class.

A class not only has values--it also has capabilities. Just as you know that an int can be added, subtracted, multiplied, and divided, a Game can be set up, played, restarted, quit, and saved.

Interface Versus Implementation

We draw a sharp distinction between the declaration of a class and its implementation. The declaration of a class tells the compiler about the attributes of the class and what its capabilities are. We often refer to this declaration as the class's interface.

Every method that is declared in the interface must be implemented: You must write the code that shows how it works.


interface--The declaration of the methods of a class

implementation--The code showing how the class methods work


Clients

Classes provide services to clients of the class. The client of your class is any programmer (even you!) who creates instances of your class in his code.


NOTE: It is regrettable that the generic pronoun in Standard English is masculine, and this is especially exacerbated by the fact that the programming profession is disproportionately male. Please understand that the masculine pronoun is intended to be generic.

Programmers use the term client in many ways. For example, if class A calls a method in Class B, we say that A is a client of B. If I write class A and I call code you wrote in class B, I am a client of your code. If my computer calls code running on your computer, my computer is a client of your (server) computer. And so on.

All these share the same essential characteristic: The client receives a service from the server.


The client of your class needs to know what your class does, but not how it works. If you create an employee class, your client needs to know that the employee can tell him his hire date, but your client does not need to know how your employee class keeps track of that date. You can store it in memory, on disk, or in a central database, but that is not important to your client. He cares about the interface (can supply date), not the implementation (retrieve date from file). Thus, the client treats your code as a black box.


client--Any code that makes use of a class


Looking at the Code

Before we discuss classes, let's take a quick look at the new code that is declaring the Game class. Once again, there is much here that will be new, but by reading through the code you can get a good idea of how it works--even before we go through it in detail.

Listing 4.1 Game.h

0:  class Game  
1:  {
2:   public:
3:      Game();
4:      ~Game();
5:      void   Play();
6:  
7:      bool    duplicatesAllowed;
8:      int        howManyLetters;
9:      int        howManyPositions;
10:      int        round;
11:  };

Declaring the Class

A class is declared by writing the keyword class, followed by the class name and an opening brace (as shown at line 0). The declaration is ended by a closing brace and a semicolon.


NOTE: The keyword public is needed as it is shown in line 2. We'll cover what this word does in a later chapter. For now, please be sure to place the keyword public, followed by a colon, at the start of every class declaration.


Classes and Objects

A class is a type. When you make instances of that type, they are called objects. In fact, the action of creating an object is called instantiation.


class--Defines a new type

object--An instance of the type defined by a class

instantiation--Creating an instance of a class: an object


Novice programmers often confuse classes with objects. The type of something (the class) tells you what it is (cat), what it can do (purr, eat, jump), and what attributes it has (weight and age). Individual objects of that type have specific objects (nine pounds, two years old.)

Member Variables

When this code was in main(), you had a number of local variables: duplicatesAllowed, howManyLetters, howManyPositions, and round.

These variables are now moved into the class, and they become members of the class itself starting at line 7.

Member variables represent attributes of the objects of that class type. In other words, we are now saying that every Game object will keep track of whether duplicates are allowed in that game, how many letters and how many positions are to be used in the game, what the current round is, and that these are the attributes of the class Game.


member variable--Data that is owned by a particular object of a class, and which represents attributes of that class.


Member variables are different from normal variables only in that they are scoped to a specific class. This is actually a very powerful aspect of object-oriented programming. The details of these values and their management are now delegated to the Game class and can be made invisible to the clients of that class.

Member Methods or Functions

The Game class has two principal activities:

You provide these capabilities to a class by giving the class member methods, which are also called member functions. A member method is a function that is owned by a class--a method that is scoped to a particular class.


NOTE: When we say that a member method is scoped to a class, we mean that the identifier that is the member method is visible only within the context of the class or an object of that class.


It is through these member methods that an object of a class achieves its behavior.

The Size of Objects

The size of an object is the sum of the sizes of the member variables that are declared for its class. Thus, if an int is 4 bytes and your class declares three integer member variables, each object is 12 bytes. Functions have no size.

Files

You create a class in two steps: First, the interface to the class is declared in a header file; second, the member methods are created in a source code file.


NOTE: Header file--A text file that contains the class declaration. Traditionally named with the .h extension

Source file--A text file that contains the source code for the member methods of a class. Traiditionally named with the .cpp extension


The header file typically has an extension of .h or .hpp, and the source code file has the extension .cpp. So for your Game class, you can expect to find the declaration in the file Game.h and the implementation of the class methods in Game.cpp.

Constructors

It is not uncommon for a class to require a bit of setting up before it can be used. In fact, an object of that class might not be considered valid if it hasn't been set up properly. C++ provides a special method to set up and initialize each object, called a constructor, as shown at line 3.

In this case, you want the constructor to initialize each of the member variables. For some member variables, you'll hard wire a reasonable value; for example, you'll keep track of what round of play you are on, and of course, you'll start with round 1.


NOTE: Hard wire is a programming term that means that the value is written into the code and doesn't change each time you run the program.


For other member variables, you must ask the user to choose an appropriate starting value. For example, you'll ask the user to tell you whether duplicates are allowed, how many letters are to be used, and how many positions are to appear in the secret code.

A constructor (line 3) has the same name as the class itself, and never has a return value.


NOTE: The absence of a return value does not, in this case, mean that it returns void. Constructors are special: They have no return value. There are only two types of methods for which this is true--constructors and destructors.


Destructors

The job of the destructor (line 4) is to tear down the object. This idea will make more sense after we talk about allocating memory or other resources. For now, the destructor won't do much, but as a matter of form, if I create a constructor, I always create a destructor.

Implementing the Methods

The header file provides the interface. Each of the methods is named, but the actual implementation is not in this file--it is in the implementation file (See Listing 4.2).

Listing 4.2 Game.cpp

0:  #include "Game.h"
1:  #include <iostream.h>
2:  
3:  
4:  Game::Game():
5:  round(1),
6:  howManyPositions(0),
7:  howManyLetters(0),
8:  duplicatesAllowed(false)
9:  {
10:     enum        BoundedValues  
11:     { 
12:        minPos = 2, 
13:        maxPos = 10, 
14:        minLetters = 2, 
15:        maxLetters = 26 
16:     };
17:     bool valid = false;
18:     while ( ! valid )
19:     {
20:        while ( howManyLetters < minLetters 
21:           || howManyLetters > maxLetters )
22:        {
23:           cout << "How many letters? (";
24:           cout << minLetters << "-" << maxLetters << "): ";
25:           cin >> howManyLetters;
26:           if ( howManyLetters < minLetters 
27:              || howManyLetters > maxLetters )
28:           {
29:              cout << "please enter a number between "; 
30:              cout << minLetters << " and " << maxLetters << endl;
31:           }
32:        }
33:  
34:        while ( howManyPositions < minPos 
35:           || howManyPositions > maxPos )
36:        {
37:           cout << "How many positions? (";
38:           cout << minPos << "-" << maxPos << "): ";
39:           cin >> howManyPositions;
40:           if ( howManyPositions < minPos 
41:              || howManyPositions > maxPos )
42:           {
43:              cout << "please enter a number between ";
44:              cout << minPos <<" and " << maxPos << endl;
45:           }
46:        }
47:  
48:        char choice = ' ';
49:        while ( choice != 'y' && choice != 'n' )
50:        {
51:           cout << "Allow duplicates (y/n)? ";
52:           cin >> choice;
53:        }
54:  
55:        duplicatesAllowed = choice == 'y' ? true : false;
56:  
57:        if ( ! duplicatesAllowed && 
58:           howManyPositions > howManyLetters )
59:        {
60:         cout << "I can't put " << howManyLetters;
61:         cout << " letters in " << howManyPositions;
62:         cout << " positions without duplicates! Please try again.\n";
63:         howManyLetters = 0;
64:         howManyPositions = 0;
65:        }
66:        else
67:           valid = true;
68:     }
69:  
70:  
71:  }
72:  
73:  Game::~Game()
74:  {
75:  
76:  }
77:  
78:  void Game::Play()
79:  {
80:  
81:  }

Listing 4.3 provides a short driver program that does nothing but instantiate an object of type Game.

Listing 4.3 Decryptix.cpp

0: #include <iostream >
1: #include "Game.h"
2: 
3: int main()
4: {
5:     Game theGame;
6:     return 0;
8: }

Including the Header

The compiler can't know what a Game is without the definition, which is in the header file. To tell the compiler what a Game object is, the first thing you do in the implementation file is to #include the file with the definition of the Game class, in this case Game.h (as shown on line 1 of Listing 4.2).


NOTE: It is desirable to minimize the number of header files that are included in other header files. Having many include statements within a header file can risk the creation of circular references (a includes b, which includes c, which includes a) that won't compile. This can also introduce order dependence, which means that the proper execution of your code depends on files being added in the "correct order." This makes for code that is difficult to maintain.

There is no limit to the number of header files you might want to include in implementation files, but keep the includes in your header file to a minimum.


Implementing the Constructor

A member function definition begins with the name of the class, followed by two colons (the scoping operator), the name of the function, and its parameters. On line 4 in Listing 4.2, you can see the implementation of the constructor.


scope operator--The pair of colons between the class name and the method

identifier--Any named thing: object, method, class, variable, and so on


Like all methods, the constructor begins with an open brace ({) and ends with a closing brace (}). The body of the constructor lies between the braces.

Initialization

In the exploration of variables, I talked about the difference between assignment and initialization. Member variables can be initialized as well. In fact, the constructor actually executes in two steps:

Construction is accomplished in the body of the constructor. Initialization is accomplished through the syntax that is shown: After the closing parentheses on the constructor, add a colon. For each member variable you want to initialize, write the variable name, followed by the value to which you want to initialize it (enclosed in parentheses). Note also that you can initialize multiple members by separating them with commas. There must be no comma after the last initialized value.

Thus, on line 5 in Listing 4.3, you see round initialized to the value 1, howManyPositions to the value 0, howManyLetters to the value 0, and duplicatesAllowed to the value false.


NOTE: The new line I've placed between each initialized value is only for the convenience of the programmer. I can just as easily put them all on one line, separated by spaces:


Game::Game(): 
round(1), 
howManyPositions(0),          
howManyLetters(0), 
duplicatesAllowed(false)
{

All this initialization occurs before the body of the constructor runs, beginning on line 10 of Listing 4.2 .


NOTE: We talk of methods or functions running, being executed, or being called, depending on context. These all mean the same thing: Program execution branches to the function, beginning at the first line and proceeding from there until it reaches a return statement.


Within the body of the constructor, you see that an enumerated constant, BoundedValues, is created, and a local variable, valid, is created and initialized on line 17.

This local variable, valid, will exist only for the duration of the constructor. Because this value is needed only temporarily and is not part of the permanent state of the object (it is not an attribute of the class Game), do not make it a member variable.

Just as valid is a variable that is local to the constructor, the instance of Game that is created in main()is local to main() (Listing 4.3, line 5). Declare it like you declare any other variable--by declaring its type (Game), and then the name of the object itself (theGame). You can name the object anything you want, but it is best to name it something meaningful so that the code can be easily understood.

By defining this object, you bring it into existence, and that causes the constructor to be invoked automatically.

Normally, methods are called explicitly. The constructor, however, is called implicitly when the object is created, and the destructor is called implicitly when the object is destroyed. When a method is called implicitly, the call doesn't appear in your code: It is understood to be the result of another action. Thus, when you create an object, you implicitly call the constructor; when you delete an object, you implicitly call the destructor. Not only do you not have to call these methods explicitly, you are prohibited from doing so.

There are two ways to see this explicitly. One way is to add a temporary output line to the constructor and destructor (as shown in Listing 4.4), and to main() (as shown in Listing 4.5).

Listing 4.4 Implicit Call to Constructor and Destructor

0:  #include "Game.h"
1:  #include <iostream>
2:  
3:  Game::Game():
4:       round(1),
5:       howManyPositions(0),
6:       howManyLetters(0),
7:       duplicatesAllowed(false)
8:  {
9:      cout << "In the Game constructor\n"" << endl;
10:  }
11:  
12:  Game::~Game()
13:  {
14:      cout << "In the Game destructor\n" << endl;
15:  }
16:  
17:  void Game::Play()
18:  {
19:  
20:  }

Listing 4.5 Driver Program for Listing 4.4

0:  #include <iostream>
1:  #include "Game.h"
2:
3:  using namespace std;
4:
5:  int main()
6:  {
7:      cout << "Creating the game\n" << endl;
8:      Game theGame;
9:      cout << "Exiting main\n" << endl;
10:      return 0;
11:  }
Creating the game
In the Game constructor
Exiting main
In the Game destructor

Here we've stripped the constructor down to do nothing except print an informative message. As you can see, creating the Game object causes the constructor to be invoked. Returning from main() ends the function and implicitly destroys any local objects. This causes the destructor to be invoked, which prints an equally informative message.

Using the Debugger

Although this works, it is tedious to add these printout messages; in any case, you can only infer the effect because you don't actually see the constructor being invoked. The debugger is a far more powerful tool.

Load Listings 4.1, 4.2, and 4.3 into a project and compile, link, and run it. Now, put a break point on line 5 in Listing 4.3--the creation of the Game object (see Figure 4.?). You are ready to see what this does, so step into the function call. You find yourself at the opening brace to the constructor.

The debugger is a powerful tool for learning C++. It can show you explicitly what is happening in your program as it runs. Because you'll be using the debugger throughout this book, you might want to take a few minutes and read the documentation that came with your programming environment to learn more about how to use your debugger.

Examining the Constructor

Careful examination of the constructor reveals that you have, essentially, duplicated the logic you had in main() in the preceding chapter. The one exception is that you are now capturing and storing the user's preferences in member variables as shown on lines 25, 39, and 52. These member variables are part of the object and will, therefore, persist and contain these values after the constructor returns.

The Other Methods

The Game object has two other methods: a destructor and the Play() method. At this time neither of these methods has any action, and you'll note that Play() is not yet called. This stubbed out function exists only to remind the programmer of his intent--that eventually this class will include a meaningful Play() method.


NOTE: When a programmer wants to show a method or function but does not want to do the work of implementing that function, he stubs it out: He creates a stub function that does nothing more than return, or at most prints, a message "in myTestMethod" and then returns.


Storing the Pattern

The computer creates a pattern that the human player guesses. How is this pattern to be stored? Clearly, you need the capability to store between minLetters and maxLetters characters as a pattern against which you can compare the human's guesses.


NOTE: Let me explain the preceding sentence because this is exactly how programmers talk about a problem like this: "The capability to store between minLetters and maxLetters characters." This sentence is easier to understand if we use sample values: If minLetters is 2 and maxLetters is 10, this sentence means that you need the capability to store between 2 and 10 letters in a pattern.

Programmers become comfortable using variables rather than absolute values in their formulations of a problem. The astute reader might also note that in fact we store these values (minLetter and maxLetter) as constants, not variables. That is true, but the values can vary from one compiled version to another, so they are variable in the more general sense.


So how do you store the computer's secret code? Let's assume that the player chooses seven possible letters with five positions, and the computer generates a secret code of acbed. How do you store this string of letters?

You have several options. You can use the built-in array class or the standard library string, you can create your own data structure to hold the letters, or you can use one of the standard library collection classes.

The rest of this chapter examines arrays in some detail; in coming chapters you'll turn to other alternatives.

What Is an Array?

An array is a fixed-size collection of data storage locations, each of which holds the same type of data. Each storage location is called an element of the array.


Array--A fixed size collection of data


You declare an array by writing the type, followed by the array name and the subscript. The subscript is the number of elements in the array, surrounded by square brackets. For example

long LongArray[25];

declares an array of 25 long integers, named LongArray. When the compiler sees this declaration, it sets aside enough memory to hold all 25 elements. Because each long integer requires 4 bytes, this declaration sets aside 100 contiguous bytes of memory, as illustrated in Figure 4.1.


subscript--The number of elements in an array


Figure 4.1 Declaring an array.

Initializing Arrays

You can initialize a simple array of built-in types, such as integers and characters, when you first declare the array. After the array name, put an equal sign (=) and a list of comma-separated values enclosed in braces. For example

int IntegerArray[5] = { 10, 20, 30, 40, 50 };

declares IntegerArray to be an array of five integers. It assigns IntegerArray[0] the value 10, IntegerArray[1] the value 20, and so on.

If you omit the size of the array, an array that is just big enough to hold the initialization is created. Therefore, if you write

int IntegerArray[] = { 10, 20, 30, 40, 50 };

you create exactly the same array as you did in the preceding example.

If you need to know the size of the array, you can ask the compiler to compute it for you. For example

int IntegerArrayLength = sizeof(IntegerArray)/sizeof(IntegerArray[0]);

sets the int variable IntegerArrayLength to the result that is obtained by dividing the size of the entire array by the size of each individual entry in the array. That quotient is the number of members in the array.

You cannot initialize more elements than you've declared for the array. Therefore,

int IntegerArray[5] = { 10, 20, 30, 40, 50, 60};

generates a compiler error because you've declared a five-member array and initialized six values. You can, however, write

int IntegerArray[5] = { 10, 20};

Uninitialized array members have no guaranteed values; therefore, any value might be in an array member if you don't initialize it.

Initializing Character Arrays

You can use a special syntax for initializing character arrays. Rather than writing

char alpha[] = { 'a', 'b', 'c' };

you can write

char alpha[] = "abc"; 

This creates an array of four characters and initializes with the three letters shown, followed by a NULL character. It is exactly as if you had written

char alpha[4] = {'a','b','c',0}

It adds the NULL because NULL-terminated strings have special meaning in C and C++.

C-Style Strings

C++ inherits from C the capability to create strings, which are meaningful groups of characters used to store words, phrases, and other strings of characters. These strings are represented in C and C++ as NULL-terminated arrays of characters. The old C library string.h, still a part of C++, provides methods for manipulating these strings: copying them, printing them, and so on.

The new Standard Library now includes a far better alternative: the string class. Objects of type string offer all the functionality of old C-style NULL-terminated arrays of characters, but with all the benefits of being well-defined types. That is, the new libraries are object-oriented, type safe, and well encapsulated.

You'll look at string objects in some detail as we go forward, and along the way I'll review some of the details of how C-style strings are used. For now, you're actually using this array of characters as a simple array, and it is not NULL-terminated. You are using the array as a collection. The objects that are being collected happen to be characters, but they can just as easily be integers or anything else you can store in an array.

Array Elements

You access each of the array elements by referring to an offset from the array name. Array elements are counted from zero. Therefore, the first array element is arrayName[0].

In the version of the program we'll examine for the rest of this chapter, you'll add a member variable solution, which will hold an array of characters that represent the solution to the code generated by the compiler.

The declaration for that array is

    char solution[maxPos+1];

This creates an array of characters that can hold exactly one more than maxPos characters. maxPos is a symbolic constant defined to 10, so this defines an array of 11 characters. The 11th character is the NULL character.

Because arrays count from offset 0, the elements in this array are solution[0], solution[1], solution[2]...solution[9].

Writing Past the End of an Array

When you write a value to an element in an array, the compiler computes where to store the value, based on the size of each element and the subscript. Suppose that you ask to write over the value at solution[5], which is the sixth element. The compiler multiplies the offset (5) by the size of each element. Since this is a char array, each element is 1 byte, so the math is fairly simple. The compiler then moves that many bytes (5) from the beginning of the array and writes the new value at that location.

If you ask to write at solution[12], the compiler ignores the fact that there is no such element. It computes how far past the first element it is to look, and then writes over whatever is at that location. This can be virtually any data, and writing your new value there might have unpredictable results. If you're lucky, your program will crash immediately. If you're unlucky, you'll get strange results elsewhere in your program, and you'll spend weeks trying to find the bug.

Fence Post Errors

It is so common to write to one past the end of an array that this bug has its own name. It is called a fence post error. This refers to the problem in counting how many fence posts you need for a 10-foot fence if you need one post for every foot: Most people answer ten, but of course you need 11. Figure 4.2 makes this clear.

Figure 4.2 Fence post errors.

This sort of "off by one" counting can be the bane of any programmer's life. Over time, however, you'll get used to the idea that a 25-element array counts only to element 24, and that everything counts from zero.


NOTE: Fence post errors are responsible for one of the great misunderstandings of our time: when the new millennium begins. A millennium is 1,000 years. We are ending the second millennium of the Common Era, and we are about to start the third--but when, exactly?

The first Millennium began with the year 1 and ended with the year 1000. The second Millennium runs from 1001 to 2000. The third will begin on January 1, 2001. (Don't tell the newspapers.)

Of course, to a C++ programmer this is all wrong. We begin counting with 0, and 1K is 1,024, thus the third C++ Millennium begins on January 1, 2048. Call it the Y2K2 problem.

Some programmers refer to ArrayName[0] as the zeroth element. Getting into this habit is a big mistake. If ArrayName[0] is the zeroth element, what is ArrayName[1], the oneth? If so, when you see ArrayName[24], will you realize that it is not the 24th element, but rather the 25th? It is far better to say that ArrayName[0] is at offset zero and is the first element.


Generating the Solution

Now that you have an array to hold the solution, how do you add letters to it? You want to generate letters at random, and you don't want the user to be able to guess the solution. The C++ library provides a method, rand(), which generates a pseudo-random number. It is pseudo-random in that it always generates numbers in the same predictable order, depending on where it starts--but they appear to be random.

You can increase the apparent randomness of the numbers that are generated if you start the random number generator with a different starting number (which we call a seed number) each time you run the program.

You provide rand() with a seed number by first calling srand() and passing in a value. srand (seed random) gives the random number generate a starting point to work from. The seed determines the first random number that will be generated.

If you don't call srand() first, rand() behaves as if you have called srand() with the seed value 1.

You want to change the seed value each time you run the program so that you'll invoke one more library function: time(). The function time returns the system time, expressed as a large integer.


NOTE: Interestingly, it actually provides you with the number of seconds that have elapsed since midnight, January 1, 1970, according to the system clock. This date, 1/1/1970, is known as the epoch, the moment in time from which all other computer dates are calculated.


The time() function takes a parameter of type time_t, but we don't care about this because it is happy taking the NULL value instead:

         srand( (unsigned)time( NULL ) );

The sequence, then, is to call time(), pass in NULL, cast the returned value to unsigned int, and pass that result to srand(). This provides a reasonably random value to srand(), causing it to initialize rand() to a nearly-random starting point.


NOTE: Let's talk about casting a value. When you cast a value to unsigned you say to the compiler, "I know you don't think this is an unsigned integer, but I know better, so just treat it like one." In this case, time() returns the value of type time_t, but you know from the documentation that this can be treated as an unsigned integer--and an unsigned integer is what srand() expects. Casting is also called "hitting it with the big hammer." It works great, but you've disconnected the sprinklers and disabled the alarms, so be sure you know what you're doing.


Now that you have a random number, you need to convert it into a letter in the range you need. To do this, you'll use an array of 26 characters, the letters a-z. By creating such an array, you can convert the value 0 to a, the value 1 to b, and so on.

Quick! What is the value of z? If you said 25, pat yourself on the back for not making the fence post error of thinking it would be 26.

We'll call the character array alpha. You want this array to be available from just about anywhere in your program. Earlier we talked about local variables, variables whose scope is limited to a particular method. We also talked about class member variables, which are variables that are scoped to a particular object of a class. A third alternative is a global variable.


global variable--A variable with no limitation in its scope--visible from anywhere in your program


The advantage of global variables is that they are visible and accessible from anywhere in your program. That is also the bad news--and C++ programmers avoid global variables like the plague. The problem is that they can be changed from any part of the program, and it is not uncommon for global variables to create tricky bugs that are terribly difficult to find.

Here's the problem: You're going along in your program and everything is behaving as expected. Suddenly, a global variable has a new and unexpected value. How'd that happen? With global variables, it is difficult to tell because they can be changed from just about anywhere.

In this particular case, although you want alpha to be visible throughout the program, you don't want it changed at all. You want to create it once and then leave it around. That is just what constants are for. Instead of creating a global variable, which can be problematic, you'll create a global constant. Global constants are just fine:

const char alpha[] = "abcdefghijklmnopqrstuvwxyz";

global constant--A constant with no limitation in its scope--visible from anywhere in your program.


This creates a constant named alpha that holds 27 characters (the characters a-z and the terminating NULL). With this in place,

alpha[0]

evaluates to a, and

alpha[25]

evaluates to z.


NOTE: We'll include the declaration of alpha in a new file called definedValues.h, and we'll #include that file in any file that needs to access alpha. This way, we create one place for all our global constants (all our defined values), and we can change any or all of them by going to that one file.


Listing 4.5 Adding Characters to the Array

0:  for ( i = 0; i < howManyPositions; )
1:  {
2:      int nextValue = rand() % (howManyLetters);
3:      char c = alpha[nextValue];
4:      if ( ! duplicatesAllowed && i > 0 )
5:      {
6:          int count = howMany(solution, c);
7:          if ( count > 0 )
8:              continue;
9:      }
10:      // add to the array
11:      solution[i] = c;
12:      i++;
13:  }
14:  solution[i] = '\0';
15:  
16:  }

On line 0 you create a for loop to run once for each position. Thus, if the user has asked for a code with five positions, you'll create five letters.

On line 2 you call rand(), which generates a random value. You use the modulus operator (%) to turn that value into one in the range 0 to howManyLetters-1. Thus, if howManyLetters is 7, this forces the value to be 0, 1, 2, 3, 4, 5, or 6.

Let's assume for the purpose of this discussion that rand() first generates the value 12, and that howManyLetters is 7. How is the value 12 turned into a value in the range 0 through 6? To understand this, you must start by examining integer division.

Integer division is somewhat different from everyday division. In fact, it is exactly like the division you originally learned in fourth grade. "Class, how much is 12 divided by seven?" The answer, to a fourth grader, is "One, remainder five." That is, seven goes into 12 exactly once, with five "left over."


integer division--When the compiler divides two integers, it returns the whole number value and loses the "remainder."


When an adult divides 12 by 7, the result is a real number (1.714285714286). Integers, however, don't have fractions or decimal parts, so when you ask a programming language to divide two integers, it responds like a fourth grader, giving you the whole number value without the remainder. Thus, in integer math, 12/7 returns the value 1.

Just as you can ask the fourth grader to tell you the remainder, you can use the modulus operator (%) to ask your programming language for the remainder in integer division. To get the remainder, you take 12 modulus 7 (12 % 7), and the result is 5. The modulus operator tells you the remainder after an integer division.

This result of a modulus operator is always in the range zero through the operand minus one. In this case, zero through seven minus one (or zero through six). If an array contains seven letters, the offsets are 0-6, so the modulus operator does exactly what you want: It returns a valid offset into the array of letters.

On line 3 you can use the value that is returned from the modulus operator as an offset into alpha, thus returning the appropriate letter. If you set howManyLetters to 7, the result will be that you'll always get a number between zero and six, and, therefore, a letter in the range a through g--exactly what you want!

Next, on line 4 you check to see whether you're allowing duplicates in this game. If not, enter the body of the if statement.

Remember, the bang symbol (!) indicates not, so

if ( ! duplicatesAllowed )

evaluates true if duplicatesAllowed evaluates false. Thus, if not, duplicatesAllowed means "if we're not allowing duplicates." The second half of the and statement is that i is greater than zero. There is no point in worrying about duplicates if this is the first letter you're adding to the array.

On line 6 you assign to the integer variable count the result of the member method howMany(). This method takes two parameters--a character array and a character--and returns the number of times the character appears in the array. If that value is greater than zero, this character is already in the array and the continue statement causes processing to jump immediately to the top of the for loop, on line 0. This tests i, which is unchanged, so proceed with the body of the for loop on line 2, where you'll generate a new value to try out.

If howMany() returns zero, processing continues on line 11, where the character is added to solution at offset i. The net result of this is that only unique values are added to the solution if you're not allowing duplicates. Next, i is incremented (i++) and processing returns to line 0, where i is tested against howManyPositions. When i is equal to howManyPositions, the for loop is completed.

Finally, on line 14 you add a NULL to the end of the array to indicate the end of the character array. This enables you to pass this array to cout, which prints every character up to the NULL.


NOTE: To designate a NULL in a character array, use the special character '\0'. To designate NULL otherwise, use the value 0 or the constant NULL.


Examining the Defined Values File

Take a look at Listing 4.6, in which we declare our constant array of characters alpha.

Listing 4.6 definedValues.h

0:  #ifndef DEFINED_VALUES
1:  #define DEFINED_VALUES
2:  
3:  #include <iostream>
4:  using namespace std;
5:  
6:  const char alpha[] = "abcdefghijklmnopqrstuvwxyz";
7:  
8:  const int minPos = 2;
9:  const int maxPos = 10;
10:  const int minLetters = 2;
11:  const int maxLetters = 26;
12:  
13:  #endif

This listing introduces several new elements.On line 0 you see the precompiler directive #ifndef. This is read "if not defined," and it checks to see whether you've already defined whatever follows (in this case, the string DEFINED_VALUES).

If this test fails (if the value DEFINED_VALUES is already defined), nothing is processed until the next #endif statement, on line 13. Thus, the entire body of this file is skipped if DEFINED_VALUES is already defined.

If this is the first time the precompiler reads this file, that value will not yet be defined; processing will continue on line 2, at which point it will be defined. Thus, the net effect is that this file is processed exactly once.

The #ifndef/#define combination is called an inclusion guard, and it guards against multiple inclusions of the same header file throughout your program. Every header file needs to be guarded in this way.


NOTE: Inclusion guards are added to header files to ensure that they are included in the program only once.


We intend to include the definedValues.h header file into all our other files so that it constitutes a global set of definitions and declarations. By including, for example, iostream.h here, we don't need to include it elsewhere in the program.

On line 6 you declare the constant character array that was discussed earlier. On lines 8-11 you declare a number of other constant values that will be available throughout the program.


Contents

© Copyright 1999, Macmillan Computer Publishing. All rights reserved.