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

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

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

88 CHAPTER 5

Language Note: You may be puzzled by that use of &, particularly if you are already familiar with C. In C++ the ampersand is used for three distinct purposes (we say that it is overloaded); they can always be distinguished by the context. The ampersand is a logical and operator when placed between two values; it is an address-of operator when used before a variable (so &data would mean ‘use the address of the object that data refers to’); and in the context of a declaration it converts the preceding type into a reference type. The special quality of C++ reference types is that the objects they refer to must already exist. In the context of a parameter, this means that the function will use an object supplied by an argument provided by the function call. All other (non-reference) parameters are value parameters. That means that the function will use a copy of the supplied argument.

It is not normally possible to copy a stream object. Having more than one object connected to a specific data source or data sink would normally be a recipe for chaos. We deal with that problem by passing stream objects around by reference. In this case, the parameter will be a reference to some already existing std::istream object. Remember that references do not use new objects; they just provide new names to access already existing objects. When we call get int( ), we will need to provide a suitable std::istream object that get int( ) can use as a source of data.

The final semicolon limits the above source-code statement to being just a declaration and nothing more. One of the interesting features of function declarations in C++ is that they provide the compiler with enough information for using the function in source code even though they do not provide enough information for the final program. For example, the following code should compile:

1 // written by FGW 25/08/04

2 #include <istream>

3 #include <iostream>

4

5 int get_int(std::istream & data_source);

6

7 int main( ){

8try {

9int i(0);

10i = get_int(std::cin);

11std::cout << "The input was " << i << ".\n";

12}

13catch(...){

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

15}

16}

When you create a project and try this code, you will find that it compiles. If you try to link it or execute the program, the linker will complain that it cannot find a definition of get int( ). C++ allows us to separate declarations from definitions. It is normal to take advantage of this so that we can change the definition if we want to without having to recompile an entire program. We might need to change a definition because we discovered an error in the existing definition. Even if there were no error, we might want to change the definition because we have discovered a way to improve on our earlier efforts.

It would be a mistake to consider that the separation of definition and declaration is unimportant. For example, Java programmers may feel that this C++ mechanism serves little purpose because Java does not make a clear distinction (though Java interfaces are sometimes used as a substitute). Industrial-quality programs can be very large and take many hours to compile from scratch. The C++ (and C) mechanism of separation of declaration and definition allows us to limit recompilation to just those files that have changed. We leave it to the linker to put all the parts together. As a result, missing parts might not be noticed until link time.

In this case, the missing part is the code that specifies how get int( ) carries out its task. We used the

function at line 10, and the compiler was able to code a call to a function using std::cin as the argument

WRITING FUNCTIONS IN C++

89

for the std::istream parameter. The compiler then inserts a request to the linker to find the definition and adjust the code to use it. The linker sees the request and looks among the provided object code (compiled files and libraries). It issues a suitable error message when it does not find a definition for the requested function.

The separation of compiling and linking is one of the ways that compiled programs differ from interpreted ones. Generally, an interpreted language – such as many versions of BASIC – only recognizes that something is missing when the program is running. This may be satisfactory in some cases but it would be a disaster in others. A program controlling a nuclear power station must know before the event that the definition of the function providing an emergency close-down exists.

What happens at each stage of the process differs from implementation to implementation. The latest tools from Microsoft tend to delay much more of the work until link time because that makes it easier to mix pieces of code written in different languages. There is no clear-cut distinction between interpreting and compiling. For example, it is usually possible to interpret C++ source code. By that, I mean, feed your source code in and get immediate action. However, it is not normal to do so, because we did not design C++ to work efficiently as an interpreted language. It is possible to compile languages, such as Python and Perl, that are designed to run through an interpreter. However, doing that may be hard work and not particularly efficient, because those languages are not designed to be compiled.

Here is a simple definition of get int( ):

int get_int(std::istream & in){ int value(0);

in >> value;

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

}

There are two ways out of this function. The first is when the code detects that something went wrong during input. Our current strategy for this is to throw a std::exception object. Later we will provide refinements that allow us to handle this problem rather more effectively. The second way out is to return the value from the function. The return statement coupled with the return type provided by the declaration and repeated in the definition makes this work.

Note that there are two differences between the declaration and definition of get int( ). First, the definition concludes with a block of statements (called the body of the function), while the pure declaration ends with a semicolon. The second difference is that I changed the name of the parameter. Parameter names in declarations have no purpose other than to document the parameter for programmers using the function. Parameter names in definitions provide a name for the variable used in the definition code. The parameters are initialized with the arguments provided at the point of call. In this case the variable in will become a local name for the existing std::istream object provided by the caller.

In effect, the declaration of a function provides a name, and specifies what type of data is needed and what type of data will be given back. The compiler uses the declaration by finding the name and checking that the arguments provided by the caller can meet the specified requirements. It then assumes that the result of calling the function will be an object (for a reference return type) or a value of the type specified. Because the compiler knows both the kind of data being provided (the arguments) and the kind of data required (the parameter types), it can often convert the argument type to the parameter type even if they are not the same initially. For example, if the parameter is an int but the argument is a double, the compiler will insert the code necessary to convert a double to an int. Note that this kind of matching up may not be possible where the parameters are references, because we cannot normally just convert an object from one type to another even though we can convert values. A region of memory formatted for storing a double will probably make no sense as an int. We can easily change a double value to an int value by ignoring the fractional part but that does not generally work for objects (places where values can be stored).

90

CHAPTER 5

Sometimes the compiler will have to insert code to convert a return value to the type required by the caller of a function. So, for example,

double d;

d = get_int(std::cin);

will compile, and the compiler will insert code to convert the int value returned by get int( ) into a double value needed for storing in d.

The simplest way to provide a function definition is to add it at the end of the file that uses it. That has a serious disadvantage because you would need to duplicate the code in every file that uses the function. There is a second disadvantage in that one of the linker’s jobs is to ensure that programmers do not provide two definitions for the same thing. We are not quite ready to tackle this issue, so for the time being either add the definition of get int( ) to the end of the file with main( ) in it or replace the declaration with the definition (single declarations can usually be replaced by the definition because a definition is always a declaration). So either of these should compile, link, and execute:

Program 1

1 // written by FGW 25/08/04 declaration early definition delayed 2 #include <istream>

3 #include <iostream>

4

5 int get_int(std::istream & data_source);

6

7 int main( ){

8try {

9int i(0);

10i = get_int(std::cin);

11std::cout << "The input was " << i << ".\n";

12}

13catch(...){

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

15}

16}

17

18 int get_int(std::istream & in){

19int value(0);

20in >> value;

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

22return value;

23}

Program 2

1 // written by FGW 25/08/04 declaration is definition

2 #include <istream>

3 #include <iostream>

4

5A int get_int(std::istream & in){

5B int value(0);

5C in >> value;

5D if(not in) throw std::exception( );

WRITING FUNCTIONS IN C++

91

5E return value;

5F }

6

7 int main( ){

8try {

9int i(0);

10i = get_int(std::cin);

11std::cout << "The input was " << i << ".\n";

12}

13catch(...){

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

15}

16}

There is not much difference between them, but I prefer Program 1 because it will be easier to convert it to the more conventional form, where we place declarations and definitions into different files.

T R Y T H I S

Write a get double( ) function that gets a value of type double from an istream object and returns the value to the caller. Write a program to test your function. Make sure that you use the resulting executable to test that the program stops with an error message if you input something that is not a double. Note that it is not an error to type in an integer value when a double is expected: the program will silently add the missing decimal indicator (decimal point in many countries, decimal comma elsewhere) as soon as it hits input that is not a digit.

EXERCISES

1.Implement (i.e. provide a definition of) and test a function whose declaration is: int squared(int);

The function should return the square of an integer provided as an argument.

2.Write and test a function that returns the square root of the largest square less than the integer value provided as an argument. Note that you must handle the possibility that the argument is out of range; negative numbers are not suitable input for this function. Handle this case by throwing an exception.

3.Write a function that takes an unsigned integer as an argument and returns half the input value if the value is even. If the input value is odd, it must return three times the input value plus one. Now test your function with a program that prompts the user for a positive number and then repeatedly calls your function to determine the next value. It must output each value until the latest value is 1. For example, given 5 as input the output should be 16, 8, 4, 2, 1. Given 7 as input the output should be 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1.

4.Write a function that has four integer parameters in addition to an fgw::playpen & parameter. The first two provide the x- and y-coordinates for a Playpen pixel, the third parameter is for the scale to be used, and the fourth one is to provide the plotting color.

92

CHAPTER 5

5.The function for Exercise 4 can be described as plotting a point of a required size and color at the given coordinates. From that perspective it does a single thing. However, we need the answers to the questions ‘‘where?’’, ‘‘how big?’’, and ‘‘what color?’’ – from which perspective we have three things. Write functions that prompt the user for a color and for a scale (one function for each). The function to get the color should restrict valid responses to the range 0–255; out of range responses should cause the user to be asked to supply another value. Invalid responses should result in an exception being thrown. The function for scale should behave similarly but the valid range of scales is 1–64.

6.Write a function to plot a point at the coordinates given by the arguments passed in to the function, but which calls the functions from Exercise 5 to determine the color and size of the square to be plotted. Note that, as for all free functions that use the Playpen window, you will need to provide a parameter that is a reference to an fgw::playpen so that the caller can tell the function which fgw::playpen object is being used.

7.Write a function that plots a horizontal line of pixels given the starting point, the number of pixels, and the color as arguments.

8.Write a function that draws a set of parallel horizontal lines given a starting point, a length (number of pixels), a color, the gap between the parallel lines, and the number of lines.

9.By first writing a similar function to that in Exercise 8 but for vertical lines, write a function that will draw a square grid of a given mesh, color, and number of squares per row/column. Write a program that tests this grid function.

C++ Procedures

C++ does not have a separate concept of a procedure – something that acts but has no resulting value. This is not a major problem – we could solve it the way that the earliest versions of C did: simply ignore the returned value from a function whose intended purpose was to package an action.

C fixed this problem by creating a special void type that has no values and very little behavior. If we declare the return type of a function as void, we are, in effect, saying that the function is a procedure. In other words, it will do something but it will not return any usable value.

For example,

void print(std::ostream &, int i);

declares print( ) to be a function that does something with an output object and an int value. The name suggests that what it will do is print the value of i on the output object. However, whatever it does, it does not return a value. In a computer-science sense, it is a procedure.

Pure Functions

Many languages have the concept of a pure function – a function that has no side effects. A pure function takes some values, computes a result, and returns the result as a value. Pure functions deal only with values and do not touch objects. Those familiar with functional programming languages will understand this concept, will know how useful it is, and will be surprised that C++ does not provide a mechanism by which the programmer can tell the compiler that something is a pure function.

It is possible that this may change in the future; for now, you can certainly write pure functions, but the compiler is unlikely to take full advantage of your doing so.

WRITING FUNCTIONS IN C++

93

Overloading Functions

C++ allows functions to share names if other details allow the compiler to select the intended function when the programmer uses it. There are two major ways that functions can share their names. First, we can declare functions with the same name in different scopes. For example, we might reuse a name in different namespaces:

namespace fgw1{ int foo( );

}

namespace fgw2{ int foo( );

}

The two declarations of foo( ) can co-exist because the fully elaborated names are different: fgw1::foo( ) and fgw2::foo( ). Everything will be fine unless you add using declarations or using directives that allow use of the simple name foo without explicitly stating the namespace.

While this is a good motive for using namespaces – we do not have to check the use of names by other programmers – it is uninteresting otherwise. Reused names become interesting when we reuse them in the same scope. The C++ term ‘function overloading’ refers to this kind of reuse of a name.

The rule for function overloading (in a single scope) is simple: the lists of parameter types must be different. The difference may be that the overloaded functions have different numbers of parameters; or if they have the same number of parameters, at least one of them must be a distinguishable type. Here is a possible set of declarations of an overloaded function:

int foo( ); int foo(int);

int foo(int, int); double foo(double);

The compiler will have no immediate difficulty with those four declarations of functions named foo. However, if we tried to add

int foo(double);

it would complain. The only difference between that declaration and the fourth one above is that it has a different return type. A difference in return type is not a sufficient distinction to allow reuse of a function name in C++. There must be a significant difference in the parameter type lists.

Generally, if at least one parameter type is different then the compiler will be happy at the point of declaration, though there may be problems later on. However, adding

int foo(int &);

to the above overload set will result in a complaint from the compiler because there is no way to distinguish a pass by value from a pass by reference for the same underlying type. If the programmer wrote:

int main( ){ int i(0); foo(i); return 0;

}

the compiler would not know if the programmer intended the version of foo that copies the value stored in i or the one that uses the i object. The language specifies that an overloaded set of functions must not include two declarations that are only different in whether a parameter uses a value or a reference.

94

CHAPTER 5

Example of Function Overloading

There are a couple of variants on the concept of get int( ). We might want to provide a specific version that always uses std::cin as the source of data. A second thing would be to provide a version that dealt with prompting the user for data. For example, given such a function I might rewrite my numbers program as:

1 // created on 27/08/04 by FWG

2 #include <algorithm>

3 #include <exception>

4 #include <iostream>

5 #include <vector>

6

7 int main( ){

8try{

9std::vector<int> numbers;

10do{

11int const next(get_int("Next whole number (-999 to stop): "));

12if(next == -999) break;

13numbers.push_back(next);

14} while(true);

(etc.)

Given a suitable version of get int( ), line 11 will prompt the user with the supplied text, and use the returned value to initialize the immutable int object designated by next. What would be a suitable declaration?

int get_int(std::string const & prompt);

looks about right. Here is a simple definition that we could use:

int get_int(std::string prompt){ std::cout << prompt;

return get_int(std::cin);

}

Notice that this new version of get int( ) delegates the actual work to our earlier version. This is another of the fundamental principles of modern programming: reuse what you already have. If you come from a language that uses recursion, note that the above definition is not recursive; the call of get int(std::cin) is to a different function that shares a name.

One of the advantages of delegation is that we only have a single place where we need to make changes if we decide that there are improvements we can make in the basic mechanism.

Let us add another version of get int( ), one that has no parameters because it is implicitly going to use std::cin as the data source.

int get_int( );

Defining that overload is effectively trivial:

int get_int( ){

return get_int(std::cin);

}

Note that it delegates everything to our earlier get int(std::istream) version.

WRITING FUNCTIONS IN C++

95

The inline Keyword

Efficiency-conscious programmers object to such delegation because they are afraid the compiler might add the overhead for a call. There is no need to worry because C++ provides a solution. You declare the function inline and provide the definition as part of the declaration:

inline int get_int( ){return get_int(std::cin);}

The inline keyword has two effects. The more important one is that it allows the function definition to exist more than once in the program. We can declare a function many times in a program, but normally we may only define it once per program; the inline keyword allows multiple definitions. The second effect of using inline is to request (note: request, not instruct) the compiler to avoid a function call by replacing the call by the body of the function.

Modern compilers can often do a much better job of optimizing code than programmers can, so do not use inline qualification of functions unless you are very certain of what you are doing. In many cases excessive use of inline results in a program that is both larger and slower than would have resulted if the programmer had never used inline.

Pass by Value or by Reference

Look back at the version of get int( ) that has a std::string parameter. Notice that this is a value parameter (there is no & after the std::string). Passing around values that have a complex structure can be expensive in resources. It is worth considering whether we could use an original object instead. We could just as easily have written:

int get_int(std::string & prompt);

so that get int( ) will use an existing std::string object. Unfortunately, that will not work unless there is an actual existing string object. It may seem odd, but a string literal (text in double quotes) is not a std::string object in C++. The compiler is quite capable of creating a temporary unnamed std::string from a string literal. It can then use that temporary as the object used by a reference. However, we come up against a C++ safety rule: C++ forbids the use of a temporary via an unqualified reference; we must promise not to change the temporary object by using a reference to const. Extracting values from a temporary object is fine; trying to modify one is not. For this reason, we have to use:

int get_int(std::string const & prompt);

That extra const qualification assures the compiler that the definition of the function will only use prompt but not try to modify the object it references. The compiler will be happy to allow you to use get int(std::string const&) with a string-literal argument. It will create a temporary std::string object from the literal and use that for the reference-to-const parameter.

Note that a reference to const is colloquially called a const reference by many C++ programmers though this is not strictly accurate. As C++ does not have any const reference types, there is no danger of confusion.

T R Y T H I S

Write a program that uses all the varieties of get int( ). Provide suitable definitions and test that the program works as expected. Now test the three alternative versions of get int( ) that provide a prompt. Check that both the pass-by-value version and the pass-by-reference-to-const version compile and work as expected. Also, note that the pass-by-reference without the const qualification does not compile if called using a string literal, but works if the argument provided by the call is a std::string. In other words,

96

CHAPTER 5

int const number(get_int("Next number"));

fails to compile if we declare the overload as

int get_int(std::string &);

but

std::string prompt("Next number"); int const number(get_int(prompt));

works fine. However, it is verbose and many programmers do not like excessive use of mutable strings (ones that have not been declared to be const).

Resetting istream and ostream Objects

C++ stream types include a number of internal flags that allow them to keep track of various events that may happen during their use. The most important state we need to track is when an I/O operation fails. There is no point in continuing if we did not get the data we requested, and we need to address an output failure before the program loses data. It is important to note that I/O objects keep track of what has happened to them but, in general, they do not attempt to predict what will happen in their future. For example, an object using a file as a source of data will only set itself into an end-of-file state when it has actually read an end-of-file marker.

When a C++ I/O object fails, it puts itself in a dormant state. std::istream objects ignore all further attempts at extracting data until the program deals with the problem. std::ostream objects do nothing with any subsequent data sent to them until the program deals with an output failure.

By design, an I/O object that has failed does nothing more until the program deals with the failure. In effect, the program skips all attempts to use a stream object that is in a failed state.

T R Y T H I S

Type in, compile, and execute the following short program. After you have checked that it runs as expected when you correctly respond to the prompt with integer values, try responding with a floating-point value. You should see the effect of the bad input. Make sure you understand what happens.

#include <iostream>

using std::cin; using std::cout;

int main( ){ try{

for(int i(0); i != 10; ++i){ int j(0);

cout << "next integer value"; cin >> j;

cout << i << " " << j << '\n';

}

WRITING FUNCTIONS IN C++

97

}

catch(...){std::cerr << "Caught an exception" << '\n'; } return 0;

}

Now we understand what happens and why it happens, it is time to see how we can fix it. C++ stream objects have a member function clear( ), which resets the object to a working state (member functions use the dot syntax to call the function for the object in question). That is all clear( ) does. It deliberately does not do anything with the data that is waiting for processing. In other words, it does not attempt to cure the cause of the problem but simply restores the object to a working condition. However, the program still has to identify and deal with the cause of the failure.

In this case, we will know that the cause of the failure is inappropriate data blocking the input. We need to remove it. There are several ways to do this, but here is a simple function that will do for now:

void clear_cin( ){ std::cin.clear( ); std::string garbage;

std::getline(std::cin, garbage);

}

This procedure (it has a void return type) clears out the current input line from std::cin.

T R Y T H I S

Add the definition of clear cin( ) to your source code for the previous task (place it before main( ) so that the definition will double up as a declaration). Add clear cin( ); between the prompt and the cin >> j; statement. Now test the code and check that it handles incorrect input satisfactorily.

EXERCISES

In the following exercises, you are expected to write complete test programs even when you have not been explicitly asked to do so.

10.Adapt the program above so that it only calls clear cin( ) if std::cin has failed.

11.Rewrite the definition of get int( ) so that it allows three attempts at correct input before it gives up and throws an exception.

12.In the case of a general std::istream object a prompt is normally inappropriate, as is ruthlessly dumping all the rest of a line of input. Write a reset istream(std::istream & data source) function that resets the stream object and discards only the next character. Why is this function of only marginal utility?