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

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

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

168

CHAPTER 9

a copy assignment operator. That is the way to copy the value/state of one instance to a second instance of the same type. We can, and often do, provide other overloads for the assignment operator, but they are not copy assignments.

Ordinary Member Functions

Ordinary member functions provide much of the behavior of a class. By ordinary member functions, I mean member functions that are not concerned with creating, destroying, or copying class instances or values.

For now, my class card value has just one ordinary member function, called send to. As the name might suggest, this member function provides the behavior of sending the value of a card value to an

output stream. During our early development and testing, we will be sending the value of a card value to std::cout, but I have designed the send to member function to work with any type of output stream including files.

You are probably curious about the const at the end of the declaration. That declares that the member function does not alter the instance using it. Without that qualification, you will get an error if you attempt to output the value of a const instance of card value with send to( ). In effect, that terminal const promises that the function will not alter the object using it. That promise restricts what you can do in the definition of the function (if you write code that might change the object, the compiler will issue an error message), and it allows you to use the function on all instances of the class including immutable ones. Forgetting the const qualification often causes problems long after you thought you had finished designing a class. The problem only surfaces the first time you try to use the function on a const instance or a const-qualified reference to an instance of the type.

TASK 9.1

 

Test my assertion about the significance of the const qualification of a

 

 

member function: modify the example code by adding const to the definition

 

 

of specific in the code for main( ) (i.e. make it card value const

 

 

 

 

 

 

 

 

 

 

 

specific(1)), and remove the const from the end of the declaration of

 

 

send to. You should now get an error message when you try to compile the

 

 

 

 

 

 

 

code.

Implementing Constructors

There is a special syntax for implementing (defining) a constructor; it includes a facility for initializing the data members. In general, functions have parameters initialized with the arguments provided at the point of call. The special property of a constructor is that it has to initialize the data members of the instance it is creating. We must initialize data members before the process enters the body of the constructor (but after any parameters have been initialized).

The section of the definition of a constructor that provides explicit initialization of the instance’s data members is introduced by a colon (:) immediately after the closing parenthesis of the parameter list. Here are possible definitions of the three constructors for the card value type:

card_value::card_value( ):data(0){

std::clog << "Default card_value (ace of clubs) constructed.\n";

}

card_value::card_value(int i):data(i){

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

169

std::clog << "card_value constructed from " << i << ".\n";

}

card_value::card_value(card_value const & c):data(c.data){ std::clog << "card_value constructed by copying.\n";

}

Notice the syntax for identifying the member function when referring to it outside the class definition. The full name of a member function outside the scope of the class definition requires that you prefix the member function name with the class scope to which it belongs. That is the purpose of the card value:: prefix to the constructor identifier in each of the above definitions.

In the copy constructor, we need to identify the version of data that belongs to the parameter. We do that using the same dot notation that we used earlier when calling a member function for an object. If you are familiar with languages such as C and Java, you will already be used to this syntax.

Because we are in a learning situation, I have defined the bodies of the three constructors so that each reports on its use. You would not normally do that when you write production code. We call such added code instrumentation.

Notice how we initialize the data member (called data) in each case. Had I left out the :data(0) in the first case, I would have had an uninitialized int representing the value of any in the code above. We generally try to avoid writing constructors that result in uninitialized data, because accidentally accessing such data (in a member function) results in undefined behavior.

The syntax for initializing a data member of a class is to include the name of the data member in a comma-separated initialization list. This list is placed between a colon following the closing parenthesis of the parameter list and the opening brace of the body of the constructor definition. We place the initializing expression in parentheses directly after the name of the data member we wish to initialize. You should note that data members are always initialized in the order in which we declare them in the class definition, regardless of the order in which we write the initializer list. A good compiler will warn you if it spots different ordering of the two (declaration and initializer list).

Implementing a Destructor

This is trivial in this case because there is nothing to clean up. The card value type does not acquire any extra resources (other than the base memory, and that is released automatically at the end of the object’s lifetime). However, because we are learning, I have instrumented the destructor to give you an example of defining one. It will also make its automatic use in your code visible when the program runs. Here is a definition of an instrumented destructor for card value objects:

card_value::~card_value(card_value const & c) { std::clog << "card_value " << data << " destroyed.\n";

}

As we are inside a member function, we can access the private members and do so with their simple names. Therefore, in this case, data is the value held by the instance of card value that we are destroying.

Notice that we have to qualify the destructor identifier with card value:: to tell the compiler the class to which this member function belongs.

Implementing Copy Assignment, operator=

There are two special features of copy assignment. The first is that C++ requires that all overloads of operator= are in a class scope. That requirement is a subtle consequence of the second feature: if the

170

CHAPTER 9

programmer does not explicitly declare a copy assignment, the compiler will declare one implicitly. It will then attempt to generate a definition if such an assignment is required by the source code (i.e. if the programmer attempts to assign the value/state of one instance to another instance of the same type). Once you declare a copy-assignment operator, everything else is like any other member function:

card_value & card_value::operator=(card_value const & c){ std::clog << "card_value " << data << " replaced by "

<< c.data << ".\n"; data = c.data;

return *this;

}

Once again, we have to specify the class to which this definition belongs. The return type comes first, just as it would in the definition of a non-member function. The other point to note is in the return-statement: *this is a special expression that refers to the object that is invoking a member function. In this case, it will be the left-hand operand of the assignment. There is little point in debating whether this is the correct thing to return. The designers of the language say that it is (though the language allows us to have any return type we want), and the way we return the left operand is with the statement return *this;.

This is not the time to tackle exactly why *this is the object using a member function. For now, just think of it as the way C++ spells what some languages call ‘self ’, i.e. the object in use.

If I did not want to instrument this function, there would be no point in declaring and defining it, because the compiler-generated implicit declaration and definition would do the same thing.

Implementing a Member Function

All we need to do now is implement the send to member function. Here is a simple implementation:

std::ostream & card_value::send_to(std::ostream & out)const{ out << data;

return out;

}

There is not much to say about this definition. We are going to modify it later on, but this is enough to get our example working.

Add the above implementation code at the end of the file you prepared with TASK 9.2 the original example code. Everything should now work when you try to

compile and link. If it does not, you will need to correct your typos.

Now modify the code so that each of the three card value variables has a different value when the program ends. That will allow you to see that the three instances of card value are destroyed in the reverse order of their construction. You might also tidy up the code by adding a few extra statements to format the output of send to reasonably. Do not change the definition of send to in order to achieve that, but add some code to the definition of main( ).

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

171

Separate Compilation

Programming languages have different ways of handling large quantities of source code. C++ has inherited the mechanism that C uses, which is to place declarations and definitions in different files. The exception to this in C (and therefore in C++) is to treat the definition of a user-defined type, a struct (and therefore a C++ class), a union or an enum as a declaration. The added complexity in C++ is that the definitions of struct/class and union types usually include declarations of member functions. The definitions of those member functions are often referred to as the implementation of the type and are placed in a separate definition or implementation file.

The declarations and type definitions are placed in files that are called header files, and traditionally use a .h suffix for the file-name extension. Files of implementation code as well as application and library code are often loosely referred to as source files (though, strictly speaking, header files are also source files). For convenience, C++ ‘source’ files use either .cpp or .cxx as the file-name extension, to distinguish them from pure C source files, which traditionally use a .c extension. None of this will be strange to C programmers, but those coming from other languages have some learning to do.

The purpose of a header file is to provide the information that is essential for a compiler to compile user code. The implementation file largely provides the definitions of the declarations in a header file. The linker will need the compiled result, but the compiler does not need the definition in order to compile code using it. That means that an implementer can change details or correct bugs without triggering a complete recompilation of all the source code in a program. Because complete recompilation of a large program can take a great deal of time, we can save much development time by isolating implementation source code from user code. The header file makes this isolation possible.

Rather than spend time explaining the details, here is the code we have been working on in this chapter reorganized to separate the implementation of card value from code that simply uses the card value type.

The Header File

In your IDE, create a new header file called card.h (or anything else you want to call it). Copy and paste the code from your earlier source file to produce:

#include <ostream>

class card_value{ public:

card_value( );

explicit card_value(int); card_value(card_value const &); ~card_value( );

card_value & operator=(card_value const &); std::ostream & send_to(std::ostream &)const;

private: int data;

};

Strictly speaking, for the current case that is enough, but multiple inclusions of the same header file (often indirectly) into a single source code file can result in redefinition errors, so it is normal to provide what are called header guards. These consist of three lines of code, two at the start of the header file and one at the end. The idea behind a header guard is to provide a way that the compiler can identify when it has already read a header file and does not need to read it again. The initial two lines have the form:

#ifndef unique-id #define unique-id

172

CHAPTER 9

where unique-id is some name that uniquely identifies the header file. For the simple code that we will be using in this book, it is usually enough to use the name of the header file spelt in uppercase, with the dot separating the file-name extension replaced with an underscore. Therefore, I would start card.h with these two lines:

#ifndef CARD_H #define CARD_H

In plain English, those lines say: ‘‘If you have not yet had a definition of CARD H, continue and first define it (as nothing).’’ The result is that the first time the compiler sees a #include "card.h" it will continue to read in the file. However, if the code results in a subsequent attempt to include card.h it will skip the contents of the file until it reaches a line that stops it skipping. The line that stops the ‘skipping’ is:

#endif

That line matches the opening #ifndef and should be the last line of the header file. So card.h becomes:

#ifndef CARD_H #define CARD_H #include <ostream>

class card{ public:

card_value( );

explicit card_value(int); card_value(card_value const &); ~card_value( );

card_value & operator=(card_value const &); std::ostream & send_to(std::ostream &)const;

private: int data;

};

#endif

Finally notice that card.h does not #include <iostream>. That is because the definition of card value does not make any use of standard stream objects. Always limit included files and headers to those needed by the code in the file. Including extra unnecessary headers and files adds clutter and can result in unnecessary recompilation of source-code files.

The Implementation File

This file contains implementation details for items declared in the corresponding header file. It only needs to be recompiled if we make changes to the implementation source code. Any project that uses the resulting compiled code (called object code) will only need it when the linker creates the executable.

Move the definitions of the member functions of card value into a file called card.cpp. (Use the IDE to create a new C++ source-code file.) The compiler will need to know the definition of the card value

type before it will compile definitions of its member functions. We provide that information by including card.h at the start of card.cpp. Your implementation file for card value should look like this:

#include "card.h"

#include <iostream> // needed for std::clog

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

173

card_value::card_value( ):data(0){

std::clog << "Default card_value constructed.\n";

}

card_value::card_value(int i):data(i){

std::clog << "card_value constructed from " << i << ".\n";

}

card_value::card_value(card_value const & c):data(c.data){ std::clog << "card_value constructed by copying.\n";

}

card_value::~card_value( ){

std::clog << "card_value with value " << data << " destroyed.\n";

}

card_value & card_value::operator=(card_value const & c){ std::clog << "card value " << data << " replaced by "

<< c.data << ".\n"; data = c.data;

return *this;

}

std::ostream & card_value::send_to(std::ostream & out)const{ out << data;

return out;

}

Create a new project and place card.cpp in it. Note that you do not need to TASK 9.3 put card.h into the project, as the IDE will find it as a dependency (because

card.cpp includes card.h).

Compile card.cpp. Correct any typos. If you try to build an executable, the linker will complain that it cannot find main( ). Remember that every program needs a single function called main( ) that acts as the start point for the program.

The Application File

In production-level projects there are usually several (even hundreds of) application files that consist of user code that builds on top of libraries and other lower-level code. Indeed code usually consists of many layers with a single simple file containing main( ) sitting on top. That will not normally be the case in this book, because such code is usually too complicated for learning purposes.

For our current project you need to create a second C++ source-code file (call it testcard.cpp) and place this source code in it:

#include "card.h" #include <iostream>

int main( ) {

174

CHAPTER 9

try{

card_value any; card_value specific(1);

card_value another(specific); any.send_to(std::cout); specific.send_to(std::cout); another.send_to(std::cout); any = specific; any.send_to(std::cout);

}

catch(...){

std::cerr << "An exception was thrown.\n";

}

}

Notice that you now need to include iostream because our definition of main( ) uses both std::cout and std::cerr. Yes, I could have included iostream as part of card.h rather than include it in each of testmain.cpp and card.cpp. However, that would result in the inclusion of iostream every time you needed a definition of card value. That would not always be necessary. Get in the habit of limiting visibility by including only essential header files. This is particularly true when you include one header file in another.

Compile testmain.cpp and build the project (create an executable). Run

TASK 9.4 the resulting program. You will find that the resulting output is rather messy; a few extra line breaks would help, as would some more text. Please take time to tidy up the output by enhancing the code in testcard.cpp.

Comment

At first sight, we have gone to a lot of trouble to get back where we started. The benefit is that we have decoupled the implementation details of card value from users of card value. Many projects can use the card value type, while the implementers of the type can refine, debug, and improve it. The header file acts as the mediator that allows both to work separately. It allows there to be many different users of the card value type; all they need is the header file for card value and the latest compiled form (called an object file) of the implementation of card value. The users have no need to concern themselves with the internals of the card value class’s implementation. We often store object code in archives called libraries. Indeed that is what we normally mean by the term ‘library’ in a programming context.

Developing the card value Type

So far, our card value type is using a non-idiomatic mechanism for output to a stream. In C++, we expect to provide output by using << (in this context, the insertion operator). We can provide a new overload for operator<< that will do this, but we cannot do so within the scope of the card value class because overloading operators in class scope only works if the first (left-hand) operand of the operator is of the relevant class type. The first operand of operator<< (used as an inserter to a stream) has to be an ostream object. This means that we have to provide the overload we want outside the class. However, this is very simple to do because send to( ) already provides the functionality we need. Just add

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

175

inline std::ostream & operator<<(std::ostream & out, card_value const & c){ return c.send_to(out);

}

after the definition of card value in card.h. The inline keyword asks the compiler to substitute the body of this definition for any call of the function. Although inline is only a request, a compiler that did not honor the request in this case would be a very poor one. If you examine the code, you will see that it tells the compiler that it can send the value of a card value to an output stream by calling send to( ) on the card value value (the right operand) and passing in the left operand.

We call functions like this one forwarding or delegating functions. C++ programmers often make extensive use of such functions, particularly for cases such as this, where they need to reorder the arguments of a function.

There is a small extra, but essential, feature of using inline: it requires that the compiler cooperate with the linker, so that it is not an error if the linker finds the definition of the function in more than one file of object code. Without the inline qualification, we may get redefinition errors if we use the header file in more than one implementation file in a project. If you get redefinition errors at link time, check that all the header files have header guards (see above) and that any functions defined in a header file have been qualified as inline.

TASK 9.5

Add the above definition to your card.h and modify main( ) so that it uses

operator<< rather than direct calls to send to( ). Compile, link, and test

the resulting code. I think you will find that one consequence of providing support for operator<< for card value is that it is now simpler to provide tidy output.

Adding get from( )

Given that we can write a card value value to an output stream, it seems natural that we should also be able to get a card value value from an input stream. However, this reverse operation is plagued by the problem that the source of the input might be a human being (or worse still, a cat walking over a keyboard). Input will always be from a fallible source; the problems are how to handle erroneous input and whether to prompt for the input. Input from a console is usually preceded by a prompt; if the input fails validation, we normally have some mechanism for retrying. Input from a file, serial port, etc. does not normally have a prompt, and if it fails validation, we have to retreat to an error-recovery mechanism.

When faced with this kind of issue in C++, the mind should turn to function overloading to deal with the different kinds of behavior. Here is a pair of versions of a member function called get from( ) to deal with the problem.

bool get_from( ); // deal with input from std::cin

std::istream & get_from(std::istream &); // deal with general input

Add those declarations to the public part of the definition of card value. You will also need to add #include <istream> as a header in card.h because of the use of std::istream in the second declaration.

Next, let us look at defining those functions. Let me take the general case first:

std::istream & card_value::get_from(std::istream & in){ in >> data;

if(not in) throw std::exception( ); return in;

}

176

CHAPTER 9

When you add this definition to card.cpp you may also need to add #include <exception> to provide the declaration of std::exception. Eventually we will want to provide our own exception type tailored to the needs of card value, but for now, I am just using the Standard Library default exception type.

This version of get from( ) tries to read a value from the designated input. If it fails to get a value, it simply gives up and throws an exception. However, it is still not doing full validation, because it assumes that any valid int will be a valid card value value. There are two ways to deal with this issue. We can accept

any int value and then reduce it to the required range. Alternatively, we can check that the supplied value is in range and throw an exception if it is not. Here is one way to implement the first option:

std::istream & card_value::get_from(std::istream & in){ unsigned int temp;

in >> temp;

if(not in) throw std::exception( ); data = temp % 52;

return in;

}

I could have done one of two things to improve that further. I could have used the read<> function from my fgwlib library to get automatic validation of the input being appropriate to the unsigned int type. I could, and normally should, remove that magic 52. The reason that I am being a little lazy is that I know that I am going to be changing the handling of card value values in a way that makes such changes unnecessary.

Now let me deal with the special case. Here is one possible implementation:

bool card_value::get_from( ){

std::cout << "Please input a value for a card "

<< "(an integer in the range 0-51 inclusive): "; try{

get_from(std::cin);

}

catch(...){return false;} return true;

}

I am not claiming this is the only way, or even the best way, to deal with this problem. I have chosen this particular solution to illustrate a number of implementation mechanisms that are available to you. The bool return is another way to handle functions that can fail; instead of throwing an exception, we use the return from the function to report success or failure. This is a good way to deal with problems that we are likely to handle locally. In this case, we do not care why get from(std::cin) failed: if calling it results in an

exception, then this version of get from( ) failed. Another implementation point is that we can implement this version in terms of the general one by first issuing the prompt and then calling the general version. This is a common way of dealing with special cases.

Another question that arises is this: should I reset the input stream in get from( ) if it fails, and should

I do so in the get from(std::istream&) version? I think that no is usually the better answer in both cases. You do not know why it failed and so it is not your responsibility to act, particularly as doing so would hide the cause (possibly reading the end-of-file marker) from the caller of the function. The caller may want that information in order to decide how to proceed.

Implementing operator>> for card value

Now that we have the functionality of reading a card value value from an input stream, we can support operator>> to extract a value from a std::istream. We have to be careful to handle the special case of extraction from std::cin. Here is a possible implementation:

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

177

inline std::istream & operator>>(std::istream & in, card_value & c){ if(&in == &std::cin) c.get_from( );

else c.get_from(in); return in;

}

In this context the &s in the if statement are the address-of operator. The controlling expression in the if statement asks whether the left operand of this use of operator>> is std::cin. If it is, use the version of get from( ) tailored for std::cin as a source of data; otherwise, use the general form.

Note that when you add the inline definition of operator>> to card.h, you will also need to add #include <iostream> to provide the declaration of std::cin. Strictly speaking, for portability to other compilers you also need to add #include <istream>.

Language Note: Unlike C programmers, C++ programmers make relatively little use of the address-of operator. In many of the places where it would be used in C, we would instead use a reference in C++.

TASK 9.6

 

Add the declarations of get from( ) and get from(std::istream) to the

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

definition of card value in card.h. Add the definitions of those functions

 

 

 

 

 

 

 

 

 

 

 

 

 

to card.cpp. Add the inline definition of operator>>(std::istream

 

 

 

 

&, card value &) to card.h.

 

 

 

 

 

 

 

 

 

 

 

 

 

Now modify cardmain.cpp to test that the input functions work

 

 

as designed. Note that you do not need to test the general version of

 

 

get from( ) explicitly, because the special version (for std::cin) tests it

 

 

 

 

 

 

 

 

 

indirectly. Make sure you include use of operator>> for a card value.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Changing the Implementation

We have a working version of card value with support for the simple things we might do with it. There is more functionality that we could add, such as comparing two card value values to see if they are the same (operator==) or to see which comes first in some ordering of our choice (operator< and operator>), but I am going to put those aside for now.

In this section, I am going to focus on changing the implementation so that we deal with the attributes of a card value rather than the raw value we have used internally. To do this I am going to use the ideas

from the previous chapter and add an enum suit and an enum denomination to the public interface of card value. I will also need to deal with the string literals that provide names for the values of those attributes. Only the implementations of the input and output functions need these names; they are data and so should be part of the private interface of card value. They are very special data because they are a property of the class as a whole, so I will be showing you how to provide such data in a class context.

Using enums to Represent Attributes

The first step is to add the following two enum definitions to the public interface of card value:

enum suit{club, diamond, heart, spade};

enum denomination {ace, two, three, four, five, six, seven, eight, nine, ten, jack, queen, king};

Now that we have provided these two enums as local types for a card value, it makes sense to provide a constructor that will work with them. Note that the definitions of suit and denomination need to occur before any use of them. It is usually a good idea to place the definitions of such nested types near the beginning of the class definition. Here is one possible constructor declaration: