You can declare C++ variables only once within each program block. A second declaration, or redef- inition, of a variable in the same block results in a compiler error. For example, suppose you have the following loop:
for ( int index = 0; index < count; index++) {
int index = count / 6; // index is redefined in loop - compiler error! total += index;
} // end for
Redefi ning index inside the loop results in a compile-time error, since index was already defi ned in thefor statement. The same restriction on redefi nition applies to entire classes. Let’s see how we could accidentally redefi ne our PlainBox class.
If we want to use a PlainBox object in our program, for instance in our main function, we must includePlainBox.h in the fi le containing the main function, main.cpp , so the compiler knows about the public interface for our class. We also need to include the header fi le for ourPlainBox class in PlainBox.cpp so the compiler knows about the data fi elds and methods of the class when it compiles the class’s implementation.
This creates a problem, since the compiler reads our PlainBox defi nition twice, even though the class needs to be defi ned only once. We don’t know which fi le the compiler will try to compile fi rst, so both fi les must include PlainBox.h . This duplication results in the class being redefi ned when the header fi le is read again. We need a way to prevent the compiler from reading the class defi nition a second time. The #ifndef , #define , and #endif preprocessor directives provide a solution.
You are already familiar with the #include directive, which includes the contents of another fi le in the current fi le. You use #ifndef to conditionally include a class defi nition. For example, the header fi le for PlainBox in Listing C1-1 contains
#ifndef _PLAIN _BOX #define _PLAIN _BOX
as its fi rst two lines. The directive #ifndef means “If _PLAIN_BOX is not defi ned, then ...” If the com- piler had not defi ned the name _PLAIN_BOX , it would process the code that follows until it reached the #endif directive at the end of the fi le.
The #define directive defi nes the name _PLAIN_BOX . If another fi le includes the class defi nition of PlainBox , the name _PLAIN_BOX will already have been defi ned. That fi le’s #ifndef directive will cause the preprocessor to skip any of the code that follows; that is, the code will be hidden from the compiler, and so the compiler will not see our class defi nition more than once.
In this textbook, all header fi les use these preprocessor directives to protect against including class defi nitions multiple times. Though you can use any names in the directives, the name to test in #ifndef and the name defi ned by #define must be the same. We write such names in uppercase and begin them with an underscore character.
C1.2
Implementing a Solution
After we have designed our solution, the next step is to implement each of the methods we declared in the header fi le. Listing C1-2 shows the implementation, or source code, fi le for the class PlainBox . The method implementations are simple, but there are a few syntax items we should discuss.
LISTING C1-2 Implementation fi le for the PlainBoxclass
/** @file PlainBox.cpp */ #include "PlainBox.h" PlainBox::PlainBox() {
} // end default constructor
PlainBox::PlainBox(const ItemType& theItem) {
item = theItem; } // end constructor
Templates 37
The header fi le PlainBox.h contains our class declaration. We must use the preprocessor direc- tive #include to include the PlainBox class declaration so the compiler can validate our method headers and provide access to class data fi elds. The method headers must match those provided in the class declaration. Including the header fi le does not tell the compiler that the methods defi ned here are part of the PlainBox class. To do so, you must precede the constructor and method names with the class name followed by two colons—that is,
PlainBox::
The reason for this requirement is that C++ allows a source fi le to contain the implementations of methods for several classes and stand-alone functions. The namespace indicator PlainBox:: is a prefi x that indicates to the compiler that the method is a part of the PlainBox namespace. A C++
namespace is a syntax structure, such as a class, that allows you to group together declarations of
data and methods under a common name, such asPlainBox . Once a method has been defi ned as part of a namespace, it has access to all the data and methods in that namespace. Thus, the constructor and methods in Listing C1-2 have access to PlainBox ’s data fi elds and methods.
Note:
To better modularize our code, the implementation fi les in this book will contain only methods from a single class.C1.3
Templates
At this point, our PlainBox class looks similar to many of the classes you probably have imple- mented in a previous C++ course. The class works well, as long as the characters in the game want to store adouble value in a box. But what do we do if one character wants to store a double and a second has astring or MagicWand object to store? Since our current PlainBox class can store only a double , we would need to create new classes— PlainBoxForStrings and PlainBoxForWands , for example— to hold different object types. These classes would function in exactly the same way as the PlainBox class, so we could copy the code with only a few changes. For example, we have to change the typedef in each header fi le. For the PlainBoxForStrings class, the typedef would be
typedef string ItemType;
and for the PlainBoxForWands class, the typedef would be typedef MagicWand ItemType;
We also would need to change the names of the constructors and the namespace indicators to refl ect the new class names. If we had a more complex class, these changes would require a sub- stantial amount of effort and would be an error-prone process. Later, if our friend changed the
{
item = theItem; } // end setItem
ItemType PlainBox::getItem() const {
return item; } // end getItem
requirements forPlainBox , we would need to go through each of the PlainBox class variations and make the required changes.
The root of the problem in this scenario is that the programmer must know what types of objects will be stored in the box before the program is built. The programmer must then write a different class for each type of object that is to be stored. The functionality of each box is the same, but the type of data differs. Templates enable the programmer to separate the functionality of an implementation from the type of data used in the class. Listing C1-3 contains the header fi le for a template version of the classPlainBox .
LISTING C1-3 Template header fi le for thePlainBoxclass
/** @file PlainBox.h */ #ifndef _PLAIN_BOX #define _PLAIN_BOX
template<class ItemType>; // Indicates this is a template definition // Declaration for the class PlainBox
class PlainBox { private: // Data field ItemType item; public: // Default constructor PlainBox(); // Parameterized constructor PlainBox( const ItemType& theItem);
// Mutator method that can change the value of the data field
void setItem( const ItemType& theItem);
// Accessor method to get the value of the data field ItemType getItem() const;
}; // end PlainBox
#include "PlainBox.cpp" // Include the implementation file #endif
As you can see from Listing C1-3, changing our earlier defi nition of the class PlainBox in Listing C1-1 to be a template requires the change of only one line in our header fi le: The typedef was removed and replaced with a statement to indicate this class is a template:
template< class ItemType>;
However, we also must include the implementation fi le just prior to the #endif directive by writing #include "PlainBox.cpp"
This addition is necessary because the compiler does not compile a template class until it sees the client’s instantiation of the template and knows the actual data type corresponding to the
Templates 39
Programming Tip:
In your development environment, do not add the implementa- tion fi le— PlainBox.cpp , for example—to the project. It will automatically be included when it is needed.The implementation fi le PlainBox.cpp requires a few changes, but these changes all follow a pattern. Prior to each method’s defi nition, you write the same template statement:
template< class ItemType>;
to indicate that the method is a template. The namespace indicator, PlainBox<ItemType>:: , must also precede each method name to refl ect that the method’s defi nition is based on ItemType . These changes are shown in Listing C1-4.
LISTING C1-4 Implementation fi le for thePlainBoxtemplate class
/** @file PlainBox.cpp */
template< class ItemType>; PlainBox<ItemType>::PlainBox() {
} // end default constructor
template< class ItemType>;
PlainBox<ItemType>::PlainBox( const ItemType& theItem) {
item = theItem; } // end constructor
template< class ItemType>;
void PlainBox<ItemType>::setItem( const ItemType& theItem) {
item = theItem; } // end setItem
template< class ItemType>;
ItemType PlainBox<ItemType>::getItem() const {
return item; } // end getItem
To instantiate an instance of PlainBox , you write the data type of the item to be placed in a box, surrounded by angle brackets:
PlainBox <double> numberBox; // A box to hold a double PlainBox<string> nameBox; // A box to hold a string object PlainBox<MagicWand> wandBox; // A box to hold a MagicWand object
Methods of these box instances are invoked as before: double health = 6.5;
string secretName = "Rumpelstiltskin"; nameBox.setItem(secretName);
MagicWand elfWand;
wandBox.setItem(elfWand);
Note:
By using templates, you can defi ne a class that involves data of any type, even data types that are created after you designed and implemented your class.C1.4
Inheritance
Now that the class PlainBox has been written, we will look at the other two boxes. The toy box is very similar to our plain box, but it has an additional characteristic, color, which we can represent with an enumerated typeColor . We set the color of our toy box when it is created. The characters in the game can ask about the box’s color but cannot change it. Here is the UML notation for those methods:
+getColor(): string
+setItem(ItemType theItem) +getItem(): ItemType
The methods setItem and getItem behave exactly the same as the similarly named methods imple- mented for thePlainBox class. We can reuse the code in PlainBox by using inheritance. Inheritance allows us to reuse and extend work we have already completed and tested.