Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

You Can Program In C++ (2006) [eng]

.pdf
Скачиваний:
80
Добавлен:
16.08.2013
Размер:
4.33 Mб
Скачать

178

CHAPTER 9

card_value(denomination, suit);

Here is a possible definition of the extra constructor:

card_value::card_value(denomination d, suit s):data(s*13 + d){ std::clog << "card_value constructed from attributes: "

<< d << " of " << s << "s.\n";

}

One question that class designers need to consider is to what extent they should protect the user from stupidity. Should we check that the denomination and suit values provided as arguments are valid ones? I have not done so here, but if you think there should be checks, you need to add checks that s and d are within the relevant ranges and throw an exception if they are not. Throwing an exception is about the only way of reporting a problem from within a constructor.

You might be tempted to remove one or more of our earlier constructors from the definition of card value. In general, you should not remove or modify a declaration in a class definition: doing so would very likely break existing code that uses the class.

TASK 9.7

Add the enums and the declaration of the new constructor to the definition

of card value in card.h. Add the definition of the new constructor to

card.cpp. Make sure that card.cpp still compiles. Now add this line to the definition of main in cardmain.cpp:

card_value yac(5, 2);

Try to compile cardmain.cpp. You now get an error because there are no implicit conversions from the int literals of 5 and 2 to card value::denomination and card value::suit. (Note that you have to qualify local types with the enclosing class type when you refer to them outside class scope. Local types are members and subject to the same rules as other members.)

Replace 5 and 2 by five and heart respectively. You still get an error, though a different one. This time the compiler does not recognize five and heart as enumerators. We are outside the scope of card value, and so we must qualify the enumerators with the enclosing class name. When we edit our code so that we have

card_value yac(card_value::five, card_value::heart);

it compiles.

Names for Values

The problem with the output of our program is that it is just a card value value; we really would hope to be able to get the output to use human readable names. To do this we need to do two things: we need to provide the names (as we did in the last chapter); and we need to modify our output to use them.

We provide the names by defining static data members. static members of a class are data or functions that are for the class as a whole rather than for individual instances of a class. Here is how we provide the literal names for the enumerators. In the private part of the definition of card value, add the following two declarations:

USER-DEFINED TYPES, PART 2: SIMPLE CLASSES (VALUE TYPES)

179

static char const

*

suit_names[4];

 

static char const

*

denomination_names[13];

 

We need to add definitions of those two variables. The place to do that is in the implementation file, so add the following to card.cpp:

char const * card_value::suit_names[4]

={"club", "diamond", "heart", "spade"}; char const * card_value::denomination_names[13]

={"ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "jack", "queen", "king"};

Notice that we drop the static but add card value:: when we provide the definitions. The keyword static has the wrong meaning outside a class definition (the alternative meaning of static will be explained elsewhere), but the compiler needs to know that what we are defining here belongs to card value.

Pointers and Arrays

C++ has inherited C’s collection type, which is a linear array of the collected type. C++ also provides a rich selection of other collection facilities such as the std::vector<> that we have already used. In general, we prefer to use C++ collections rather than arrays because the former have a much richer structure and range of facilities. However, there are times when the simplicity of the array is useful.

As a general rule, you might use an array when:

the size is known and fixed at the time the source code is written;

suitable initialization can be provided.

One strong motive for using an array is that we know what the stored values should be but there is no way to compute them. The static data members suit names and denomination names in card value are excellent examples; we know what they should be because they are a fact rather than something that is computable.

We declare an array in C++ by specifying the type of the object held in the array and adding a pair of square brackets after the name being declared. We generally place the size of the array in the square brackets, but if we leave the brackets empty, the compiler determines the size by counting the provided initializers.

Initializers are provided in the declaration/definition of an array as a comma-separated list enclosed in curly braces ‘assigned’ to the array.

I do not want to spend much time on C++ pointers at this stage, but you do need a minimal amount of information in order to make sense of some of the source code in this chapter. We saw earlier that appending an asterisk to a type name creates a new type that is a pointer to the named type. A pointer value is effectively the address of the storage for the object. A pointer variable provides storage for a pointer value. Generations of programmers have been confused by pointers, which is a good reason for keeping their use to the minimum.

In most circumstances when you refer to an array in your source code, the compiler will use the address of the start of the array, and if you need to store that address you will need storage for a suitable pointer type.

For historical reasons, string literals are implicitly arrays of char, and we pass them around in source code by using the address of the place where the compiler has placed the actual string. Fortunately, the new programmer does not need a deep understanding of pointers and plain arrays in C++ in order to use them in introductory code.

Look at the definition of card value::suit names. The [4] tells us that we are defining an array of four elements. The char const * tells us that the elements are storage for the addresses of immutable chars (which includes the start addresses of immutable arrays of char because of the way that C++ uses

180

CHAPTER 9

such addresses when asked to handle an array). The = sign tells the computer that we are about to provide explicit initialization for the array. The initializer list (provided in the curly braces) consists of four string literals. As the compiler needs addresses to initialize the elements of card value::suit names, it uses the addresses of where the string literals are stored (addresses that it provides by the internal mechanism it uses for string literals).

In any context where an array of char is needed (i.e. when working with simple C-style strings rather than C++ std::string), card value::suit names[0] will be treated as an array containing the characters 'c', 'l', 'u', 'b', and a final '\0' to mark the end of the string.

The biggest problem with pointers and arrays is that a pointer used to refer to an array (through its start address) is indistinguishable from a pointer used to reference a single object.

I will be tackling pointers in more detail in Chapter 11.

Language Note: C programmers are likely to have the biggest problem with learning the C++ idioms for using pointers, because they will have a good understanding of their idiomatic use in C. However, such idioms are often dangerous in a C++ context.

Those two definitions use the special syntax for initializing a fixed-size array at the point of definition. We are also using the special support that C++ has inherited from C to handle string literals as arrays of char. When the compiler deals with the above code it will provide space to store the actual literals and then place the address of each into the relevant point of the array. The char const * specifies that we are using addresses (that is the meaning of the asterisk in this context) of chars that cannot be changed through this address value. The [4] and [13] specify the number of elements in each of the arrays.

Now we have the names available, we can go back and change the implementation of send to so that it sends named values of the attributes to the output. Here is one way to do it:

std::ostream & card_value::send_to(std::ostream & out)const{

out << denomination_names[data % 13] << " of " << suit_names[data/13]; return out;

}

Add all the above code to the relevant files. Note that you do not need to TASK 9.8 touch cardmain.cpp; users should not have to change their code because a class implementer has changed the implementation. Build the new version

and execute it. Notice that you get the new behavior for output.

Tidying Up

If you look at the output from your last task, you will realize that the instrumentation that we added to the constructors, destructor, and copy assignment is still outputting numbers. We can clean that up very easily. Here is the cleaned-up copy assignment as an example:

card_value & card_value::operator=(card_value const & c){

std::clog << "card value " << *this << " replaced by " << c << ".\n"; data = c.data;

return *this;

}

The key is that we can now send a card value value to output, rather than using the internal representation (the member called data). The other essential is that *this always represents the object using a function (being created, destroyed, etc.).

USER-DEFINED TYPES, PART 2: SIMPLE CLASSES (VALUE TYPES)

181

TASK 9.9

 

Clean up all the instrumentation in our implementation of card value so that

 

 

 

 

 

 

 

output always refers to a card value value in the form ‘x of y ’ (e.g. ‘three

 

 

 

 

 

 

 

of spades’). In particular, tidy up the instrumentation in the constructors and

 

 

 

 

the destructor. Build and execute the resulting code.

T R Y T H I S

Instrumenting code is all very well, but we do not want all those messages when we use our work to produce a program to play a card game that we intend sharing with our friends. The messages help us kept track of what is happening but are invasive in the context of a ‘release’ product.

Save a copy of card.cpp but call the copy card1.cpp. Now edit card1.cpp to remove all the instrumentation. Replace card.cpp in your project with card1.cpp. Rebuild the project. Voila, uninstrumented code.

This is just one of the payoffs for keeping implementation separate and using a header file to provide the compiler with what it needs to compile user code. In fact, all you need to do is link your application with the appropriate object file for the implementation you wish to use.

Add comparison operators for card value as members of the class. For example, you can

declare and implement operator== for card value with

bool operator==(card_value const &)const;

added to the public interface of card value, and define it in the implementation as:

bool operator==(card_value const & c)const{ return data == c.data;

}

Add that along with declarations and definitions of operator< and operator> for card value. Note that in all cases the parameter should be a const reference (or value), and the member function should be qualified as const (that const just after the closing parenthesis of the parameter list). Comparing instances should not change them.

You should find those additions straightforward. The next task is a bit more demanding: you need to update the get from(std::istream &) function so that it reads the output of

send to( ) correctly. The reason is that send to( ) may be used to store data in a file, and you want to be able to read that data back from the file by using get from(std::istream &).

As a hint, you can input a single word into a temporary variable of type std::string. The c str( ) member of std::string allows you to view a std::string object as an array

of char. The strcmp( ) library function (inherited from C) takes two null-terminated arrays of char (C-style strings) and returns zero if they are identical.

For example, strcmp(temp.c str, card value::suit names[club]) returns zero

if and only if the string stored in temp is "club".

At this point, you may want to reimplement the special version of get from( ) that uses std::cin so that the user is prompted for each of the attributes of a card value. Note that this change means that you can no longer forward to the general function.

If you are hopelessly stuck when trying this, check the answer I give at the end of this book.

Consolidation – a Point Class

Read chapters 5 and 6 of You Can Do It! from the CD to get an alternative presentation of designing a class (a 2D point class for use with my Playpen library). Then try one or more of the following exercises.

182

CHAPTER 9

EXERCISES

I have worked through the development of the card value class. At this point, you need to stop and work on a couple of classes for yourself. Here are some possibilities for you to work on. Please note that these classes are far from trivial, and so you should expect to take some time to provide complete implementations of the definitions provided. You should also plan to work incrementally rather than trying to do the whole job at once.

5.Here is the start of a class for calendar dates within a single year. You will find that it uses most of the things we have covered in designing and developing card value. You may modify the private interface if you think you have a better way to structure the data.

class date{ public:

date( );

explicit date(int day_number, bool leap_year = false); date(std::string month, int day, bool leap_year = false); date(date const &);

~date( );

date & operator=(date const &); ostream & send_to(ostream &)const;

istream & get_from( ); // uses std::cin istream & get_from(std::istream &); bool leap_year( );

private: bool leap;

unsigned int day; // counted from January 1st as 0 static signed char const

days_in_month[12]; // uses signed char as a small int static char const * month_names[12];

};

Create a suitable header file and place the date definition in it. Create a suitable implementation file and implement the date class. Now create an application file that consists of a definition of main( ) that tests all the functionality of date. You might find it easier to implement the class without managing the differences required for a leap year, then go back to add the modifications necessary to handle leap years. The provision of default arguments in the constructors should make that easier. You will also find it easier to implement date bit by bit; do not try to do it all at once but tackle it by incremental refinement and improvement (as I did for card value).

Note that I have only declared the first constructor as explicit. Because we can call that constructor with a single argument by using the default argument for leap year, we need to protect against accidental use of the constructor by the compiler as a conversion operator between an int and a date. The second constructor does not suffer from this problem, because it always needs at least two arguments.

Several functions will need to validate the data provided, to ensure that they are legitimate dates (e.g. there is no January 32nd). You will need to decide what to do with an invalid date. For example, an invalid date passed as an argument to a constructor will probably need to be handled by throwing an exception.

When you have the specified interface implemented and working correctly, spend some time on enhancements such as providing streaming operators, and increment and decrement operators (add the declarations date operator++( ); and date operator--( ); to the class definition).

USER-DEFINED TYPES, PART 2: SIMPLE CLASSES (VALUE TYPES)

183

Note that it makes no sense to add two dates together, but you can subtract one date from another to find the interval between them. So add int operator-(date const&)const; to the definition of date and provide an implementation. It also makes sense to add an integer value to a date. Add date operator+(int); to the definition of date and implement it.

There is a great deal more you could add to this class if you wanted to flesh it out to be a robust general-purpose type for use in your code. Remember that you can always add to a class and modify its implementation and private interface. However, you should not change the public interface in any way that might break existing code, e.g. by removing a public declaration or by changing the return type or the type of a parameter. You can add extra overloads of a function to handle alternative parameter types.

6.Use the date class from Exercise 5 as a basis for designing and implementing a more general date class that deals with dates across a range of years. One interesting function to include in this is one to output the day of the week of the currently stored date value.

7.Here is a skeleton definition for a class to represent color values with the primary colors (red, green, and blue – for light) treated as attributes.

class rgbcolor{ public:

rgbcolor( );

rgbcolor(int red, int green, int blue); rgbcolor(rgbcolor const &);

~rgbcolor( );

rgbcolor & operator=(rgbcolor const &); ostream & send_to(ostream &);

istream & get_from( ); // use std::cin as source istream & get_from(istream &);

int red( ); // returns current value of red component int blue( ); // returns current value of blue component int green( ); // returns current value of green component void red(int newval);

void blue(int newval); void green(int newval);

private: int red_;

int blue_; int green_;

};

Create a header file for the class definition and the declaration of support functions. Create files for the implementation and test source code. Note the overloading of the red( ), blue( ), and green( ) functions: the versions with an empty parameter list return the current value of the relevant attribute; the versions with an int parameter change the stored value of the parameter. It happens that the three attributes are stored separately, but there is no reason for a user to assume that, and if the class designer decides to store the information in some other way, they are free to do so as long as they maintain the public interface.

Though this class could form the basis for redefining the palette for Playpen, actually making the change will require more knowledge of the playpen interface than you currently have.

You can expand the functionality of rgbcolor in many ways. Please take the time to add at least one extra feature to rgbcolor.

184

CHAPTER 9

Defining Member Functions in a Class

Definition

You may wonder about the efficiency of many of the simple member functions that either return or modify an attribute, for example, the red( ), blue( ), and green( ) functions in the rgbcolor type in Exercise 7.

You can define a member function in the class definition. Doing so makes the definition implicitly inline. Most experts advise that defining member functions in a class definition is generally a poor idea. However, where no computation is involved it is more acceptable. So we could replace

int red( ); // returns current value of red component

and

void red(int newval);

with

int red( ){return red_;}

and

void red(int newval){red_ = newval;}

respectively.

However, if I change the way that the data is stored so that computation is required to extract or modify the red attribute, such definitions of members functions inside a class definition would become more suspect. Making a function inline is an optimization, and you should generally avoid hand-optimization unless the code has inadequate performance. Modern compilers are generally good (and are getting better with each release) at inlining small functions at link time even when the programmer has not suggested it.

I will generally avoid in-class definitions in the code I provide in this book, but you need to know it is possible and understand its significance when you see it in other code.

REFERENCE SECTION

The class is an important mechanism for adding user-defined types in C++. We use the keywords class and struct to declare and define class types. With a single difference, described later, the two keywords are interchangeable. We use a third keyword, union, to declare and define a very restricted user-defined type that is used to allow sharing of raw memory for different types of objects during its existence.

Declaration of a Class Name

We can declare the name of a class with one of the following forms:

class name; struct name;

There should be no difference between those two declarations, and they do not commit the programmer to using the same keyword in the corresponding definition. I say ‘‘should be no difference’’ because

USER-DEFINED TYPES, PART 2: SIMPLE CLASSES (VALUE TYPES)

185

some compilers (e.g. some versions of Visual C++) do treat them differently, so it is probably wise to be consistent even though C++ does not require you to be.

Once a declaration of a class type is visible to the compiler, you may use references and pointers to the type declared. So given the declaration

class foo;

the definition

foo * ptr_foo(0);

is fine and defines ptr foo as a pointer, initialized as a null pointer, to a foo. However, because references need to be initialized at the point of definition, you can only use a reference in the context of a declaration that is not a definition. The main cases covered by this are parameter and return types of functions (including member functions) and the declaration of reference members of a class type.

Many people call the declaration of a class name a ‘forward declaration’ to emphasize that it is only a declaration.

Definition of a Class

The definition of a class type (with either keyword) consists of declarations of data members and member functions. It can also include the definitions of nested user-defined types (including enum and union types) and the declaration of new type names using typedef. Finally, a class definition can include declarations of external types and functions as friends of the class. We write the definition of a class in the form:

class name {

// declarations of class members

};

We can replace class with struct. Note that the final semicolon is essential and must not be omitted.

The declarations in a class definition belong to one of three interfaces: public, private, and protected. Each declaration or definition of a nested type is assigned to one of those three interfaces depending on which of three access specifiers has been used most recently. If no access specifier has so far been used in the definition of a class then classes defined with the class keyword default to private access and those defined using the struct keyword default to public access. That is the only language difference between using struct and using class to define a class type.

We use the keywords public, private, and protected to identify sections of a class definition that belong to each of the interfaces. A class definition can have more than one section for a given interface. In other words, we can use any of these keywords more than once in a class definition.

The public interface consists of those members of a class that can be used outside the scope of the class. It usually consists of the declarations of member functions that provide the public behavior for instances of the class. It sometimes includes the definitions of nested types and the use of typedef to provide local names for types. It is normally poor practice to place data members in a public interface.

The private interface contains declarations and definitions of those things that the class designer wishes to control. Generally, the private interface contains the declarations of all the data members, as well as any utility functions that the class implementer wants to use but which do not constitute part of the published behavior of the class.

The protected interface is a specialist interface to support certain needs of class designers who intend that the class will be used as the basis for other class designs. You will learn more about this

186

CHAPTER 9

in Chapter 12. In general, the protected interface should not include declarations of data members. However, many people ignore that design guideline.

In addition to providing the behavior and state/value of instances of a class, it is sometimes desirable to provide some behavior and state/value for the class as a whole. Such features of a class definition are identified by the use of the keyword static. This is quite different from uses of that keyword in other places in C++.

Ordinary member functions must have an instance as part of their use. Inside an ordinary member function the instance that invoked it is called *this. The reason for the asterisk is that, for historical reasons, this is a pointer value not an object. Prefixing a pointer with an asterisk refers to the object whose address is provided by the pointer.

It is usual to consider *this (or this) as an extra parameter that every ordinary member function has. The argument that provides the initialization for this implicit parameter is the instance that precedes the dot in the syntax for using member functions. When a member function is called from within the implementation of another member function for the class, there is an implicit *this. prefixing the member-function call. For example, given the class definition

struct example( ){ void foo( ); void bar( );

// other members };

the code

void example::foo( ){ bar( );

}

is equivalent to:

void example::foo( ){ *this.bar( );

}

static Members

Class member functions (identified by using static in their declaration) do not have access to an implicit *this parameter. They cannot make use of instance data or ordinary member functions. Suppose we wanted to keep track of how many card value instances a process is using. We can do that by adding

static int count;

to the private interface of card value. We can then modify all the constructors so that they increment count (using ++count;). We also need to modify the destructor by including the statement -- count;. Now every time a new card value instance is created, count goes up by one, and every

time an instance is destroyed, it goes down by one. Remember that we only declare data in a class definition – it still has to be defined and initialized somewhere. The place to do that, in this case, is in card value’s implementation file, by adding the line:

int card_value::count(0);

That statement will create a single int object for use by the card value type and set it to zero.

USER-DEFINED TYPES, PART 2: SIMPLE CLASSES (VALUE TYPES)

187

We now need a way to ask what the current count is. To do this we provide a public static member function:

static int get_count( );

The static tells the compiler that this member function has no hidden *this parameter and so is restricted to other static members of the class. Its implementation is:

int card_value::get_count( ){return count;}

If we want to call that function in our code, we use its full name, card value::count( ). The language also allows us to use an instance and dot notation, so

int main( ){ card_value any;

std::cout << any.get_count( );

}

will work, but it is generally better style to emphasize that we are using a static member function with:

int main( ){ card_value any;

std::cout << card_value::get_count( );

}

If you do not have an instance of card value to use, that is your only option.

There is a special case where static members can be initialized in the class definition. This is the case where the static member is a const-qualified instance of some integer type. All three properties must be true: it must be a static member; it must be an integer member; and it must be const-qualified. The purpose of this special license is to allow the removal of magic integer values from class definitions by allowing integer literal values to be named in the class definition. We will see examples later in this book.

Constructors and Destructors

The job of a constructor function is to create a new instance of a class type by acquiring the necessary resources (including the base memory for the data) and initializing data members. In general, a well-designed constructor will place an instance into a safe state for use in a program. An instance of a class starts its lifetime when its constructor has finished running. We identify a constructor by using the class name. A class can have (and usually does have) multiple constructors to allow instances to be created from different initial data.

Every class has a single destructor, whose task is to release the resources used by an instance when its lifetime ends. The lifetime of an instance of a class ends when its destructor is called (not completed, just called). Generally, a destructor is called implicitly when a variable goes out of scope. Explicitly calling a destructor is uncommon. We identify a destructor by prefixing the class name with a tilde (~).

Neither constructors nor destructors can have a return type. It is normal to report a failure to construct an instance by throwing an exception. It is usually a serious design fault if a destructor can fail. Throwing an exception from a destructor is never the right answer to a destruction failure, though the language allows you to do so.