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

Beginning Visual C++ 2005 (2006) [eng]

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

More about Program Structure

If you execute this example, it produces the following apocalyptic output:

Something is wrong.

Something is terribly wrong!

Something is wrong.

The end of the world is nigh.

How It Works

As you can see, you get the default message specified in the function prototype whenever the argument is left out; otherwise, the function behaves normally.

If you have a function with several arguments, you can provide initial values for as many of them as you like. If you want to omit more than one argument to take advantage of a default value, all arguments to the right of the leftmost argument that you omit must also be left out. For example, suppose you have this function:

int do_it(long arg1 = 10, long arg2 = 20, long arg3 = 30, long arg4 = 40);

and you want to omit one argument in a call to it. you can omit only the last one, arg4. If you want to omit arg3, you must also omit arg4. If you omit arg2, arg3 and arg4 must also be omitted, and if you want to use the default value for arg1, you have to omit all of the arguments in the function call.

You can conclude from this that you need to put the arguments which have default values in the function prototype together in sequence at the end of the parameter list, with the argument most likely to be omitted appearing last.

Exceptions

If you’ve had a go at the exercises that appear at the end of the previous chapters, you’ve more than likely come across compiler errors and warnings, as well as errors that occur while the program is running. Exceptions are a way of flagging errors or unexpected conditions that occur in your C++ programs, and you already know that the new operator throws an exception if the memory you request cannot be allocated.

So far, you have typically handled error conditions in your programs by using an if statement to test some expression, and then executing some specific code to deal with the error. C++ also provides another, more general mechanism for handling errors that allows you to separate the code that deals with these conditions from the code that executes when such conditions do not arise. It is important to realize that exceptions are not intended to be used as an alternative to the normal data checking and validating that you might do in a program. The code that is generated when you use exceptions carries quite a bit of overhead with it, so exceptions are really intended to be applied in the context of exceptional, near catastrophic conditions that might arise, but are not normally expected to occur in the normal course of events. An error reading from a disk might be something that you use exceptions for. An invalid data item being entered is not a good candidate for using exceptions.

279

Chapter 6

The exception mechanism uses three new keywords:

try — identifies a code block in which an exception can occur

throw — causes an exception condition to be originated

catch — identifies a block of code in which the exception is handled

In the following Try It Out, you can see how they work in practice.

Try It Out

Throwing and Catching Exceptions

You can easily see how exception handling operates by working through an example. Let’s use a very simple context for this. Suppose that you are required to write a program that calculates the time it takes in minutes to make a part on a machine. The number of parts made in each hour is recorded, but you must keep in mind that the machine breaks down regularly and may not make any parts.

You could code this using exception handling as follows:

// Ex6_04.cpp Using exception handling #include <iostream>

using std::cout; using std::endl;

int main(void)

{

int

counts[] =

{34, 54, 0, 27, 0, 10, 0};

int

time = 60;

// One hour in minutes

for(int i = 0 ; i < sizeof counts/sizeof counts[0] ; i++) try

{

cout << endl

<< “Hour “ << i+1;

if(counts[i] == 0)

throw “Zero count - calculation not possible.”;

cout << “ minutes per item: “

<< static_cast<double>(time)/counts[i];

}

catch(const char aMessage[])

{

cout << endl

<<aMessage

<<endl;

}

return 0;

}

If you run this example, the output is:

Hour 1 minutes per item: 1.76471

Hour 2 minutes per item: 1.11111

280

More about Program Structure

Hour 3

Zero count - calculation not possible.

Hour 4 minutes per item: 2.22222

Hour 5

Zero count - calculation not possible.

Hour 6 minutes per item: 6

Hour 7

Zero count - calculation not possible.

How It Works

The code in the try block is executed in the normal sequence. The try block serves to define where an exception can be raised. You can see from the output that when an exception is thrown, the sequence of execution continues with the catch block and after the code in the catch block has been executed, execution continues with the next loop iteration. Of course, when no exception is thrown, the catch block is not executed. Both the try block and the catch block are regarded as a single unit by the compiler, so they both form the for loop block and the loop continues after an exception is thrown.

The division is carried out in the output statement that follows the if statement checking the divisor. When a throw statement is executed, control passes immediately to the first statement in the catch block, so the statement that performs the division is bypassed when an exception is thrown. After the statement in the catch block executes, the loop continues with the next iteration if there is one.

Throwing Exceptions

Exceptions can be thrown anywhere within a try block, and the operand of the throw statements determines a type for the exception — the exception thrown in the example is a string literal and therefore of type const char[]. The operand following the throw keyword can be any expression, and the type of the result of the expression determines the type of exception thrown.

Exceptions can also be thrown in functions called from within a try block and caught by a catch block following the try block. You could add a function to the previous example to demonstrate this, with the definition:

void testThrow(void)

{

throw “ Zero count - calculation not possible.”;

}

You place a call to this function in the previous example in place of the throw statement:

if(counts[i] == 0)

testThrow(); // Call a function that throws an exception

The exception is thrown by the testThrow() function and caught by the catch block whenever the array element is zero, so the output is the same as before. Don’t forget the function prototype if you add the definition of testThrow() to the end of the source code.

281

Chapter 6

Catching Exceptions

The catch block following the try block in our example catches any exception of type const char[]. This is determined by the parameter specification that appears in parentheses following the keyword catch. You must supply at least one catch block for a try block, and the catch blocks must immediately follow the try block. A catch block catches all exceptions (of the correct type) that occur anywhere in the code in the immediately preceding try block, including those thrown in any functions called directly or indirectly within the try block.

If you want to specify that a catch block is to handle any exception thrown in a try block, you must put an ellipsis (...) between the parentheses enclosing the exception declaration:

catch (...)

{

// code to handle any exception

}

This catch block must appear last if you have other catch blocks defined for the try block.

Try It Out

Nested try Blocks

You can nest try blocks one within another. With this situation, if an exception is thrown from within an inner try block that is not followed by a catch block corresponding to the type of exception thrown, the catch handlers for the outer try block are searched. You can demonstrate this with the following example:

// Ex6_05.cpp

// Nested try blocks #include <iostream> using std::cin; using std::cout; using std::endl;

int main(void)

{

int height = 0;

const double inchesToMeters = 0.0254; char ch = ‘y’;

try

// Outer try block

{

 

while(ch == ‘y’||ch ==’Y’)

 

{

 

cout << “Enter a height in inches: “;

 

cin >> height;

// Read the height to be converted

try

// Defines try block in which

{

// exceptions may be thrown

if(height > 100)

 

throw “Height exceeds maximum”;

// Exception thrown

if(height < 9)

 

throw height;

// Exception thrown

cout << static_cast<double>(height)*inchesToMeters

282

 

More about Program Structure

 

 

<< “ meters”

 

<< endl;

 

}

 

catch(const char aMessage[])

// start of catch block which

{

// catches exceptions of type

cout << aMessage << endl;

// const char[]

}

cout << “Do you want to continue(y or n)?”;

cin >> ch;

}

}

catch(int badHeight)

{

cout << badHeight << “ inches is below minimum” << endl;

}

return 0;

}

Here there is a try block enclosing the while loop and an inner try block in which two different types of exception may be thrown. The exception of type const char[] is caught by the catch block for the inner try block, but the exception of type int has no catch handler associated with the inner try block; therefore, the catch handler in the outer try block is executed. In this case, the program ends immediately because the statement following the catch block is a return.

Exception Handling in the MFC

This is a good point to raise the question of MFC and exceptions because they are used to some extent. If you browse the documentation that came with Visual C++ 2005, you may come across TRY, THROW, and CATCH in the index These are macros defined within MFC that were created before the exception handling was implemented in the C++ language. They mimic the operation of try throw and catch in the C++ language, but the language facilities for exception handling really render these obsolete so you should not use them. They are, however, still there for two reasons. There are large numbers of programs still around that use these macros, and it is important to ensure that as far as possible old code still compiles. Also, most of the MFC that throws exceptions was implemented in terms of these macros. In any event, any new programs should use the try, throw, and catch keywords in C++ because they work with the MFC.

There is one slight anomaly you need to keep in mind when you use MFC functions that throw exceptions. The MFC functions that throw exceptions generally throw exceptions of class types(you will find out about class types before you get to use the MFC. Even though the exception that an MFC function throws is of a given class type(CDBException say(you need to catch the exception as a pointer, not as the type of the exception. So with the exception thrown being of type CDBException, the type that appears as the catch block parameter is CBDException*. You will see examples where this is the case in context later in the book.

283

Chapter 6

Handling Memor y Allocation Errors

When you used the operator new to allocate memory for our variables (as you saw in Chapters 4 and 5), you ignored the possibility that the memory might not be allocated. If the memory isn’t allocated, an exception is thrown that results in the termination of the program. Ignoring this exception is quite acceptable in most situations because having no memory left is usually a terminal condition for a program that you can usually do nothing about. However, there can be circumstances where you might be able to do something about it if you had the chance or you might want to report the problem in your own way. In this situation, you can catch the exception that the new operator throws. Let’s contrive an example to show this happening.

Try It Out

Catching an Exception Thrown by the new Operator

The exception that the new operator throws when memory cannot be allocated is of type bad_alloc. bad_alloc is a class type defined in the <new> standard header file, so you’ll need an #include directive for that. Here’s the code:

// Ex6_06.cpp

#include<new> // For bad_alloc type #include<iostream>

using std::bad_alloc; using std::cout; using std::endl;

int main( )

{

char* pdata = 0;

size_t count = ~static_cast<size_t>(0)/2; try

{

pdata = new char[count];

cout << “Memory allocated.” << endl;

}

catch(bad_alloc &ex)

{

cout << “Memory allocation failed.” << endl

<<“The information from the exception object is: “

<<ex.what() << endl;

}

delete[] pdata; return 0;

}

On my machine this example produces the following output:

Memory allocation failed.

The information from the exception object is: bad allocation

If you are in the fortunate position of having many gigabytes of memory in your computer, you may not get the exception thrown.

284

More about Program Structure

How It Works

The example allocates memory dynamically for an array of type char[] where the length is specified by the count variable that you define as:

size_t count = ~static_cast<size_t>(0)/2;

The size of an array is an integer of type size_t so you declare count to be of this type. The value for count is generated by a somewhat complicated expression. The value 0 is type int so the value produced by the expression static_cast<size_t>(0) is a zero of type size_t. Applying the ~ operator to this flips all the bits so you then have a size_t value with all the bits as 1, which corresponds to the maximum value you can represent as size_t because size_t is an unsigned type. This value exceeds the maximum amount of memory that the new operator can allocate in one go so you divide by 2 to bring it within the bounds of what is possible. This is still a very large value so unless your machine is exceptionally well endowed with memory, the allocation request will fail.

The allocation of the memory takes place in the try block. If the allocation succeeds you’ll see a message to that effect but if as you expect it fails, an exception of type bad_alloc will be thrown by the new operator. This causes the code in the catch block to be executed. Calling the what() function for the bad_alloc object reference ex returns a string describing the problem that caused the exception and you see the result of this call in the output. Most exception classes implement the what() function to provide a string describing why the exception was thrown.

To handle out-of-memory situations with some positive effect, clearly you must have some means of returning memory to the free store. In most practical cases, this involves some serious work on the program to manage memory so it is not often undertaken.

Function Overloading

Suppose you have written a function that determines the maximum value in an array of values of type double:

// Function to generate the maximum value in an array of type double double maxdouble(double array[], int len)

{

double max = array[0];

for(int i = 1; i < len; i++) if(max < array[i])

max = array[i];

return max;

}

You now want to create a function that produces the maximum value from an array of type long, so you write another function similar to the first, with this prototype:

long maxlong(long array[], int len);

285

Chapter 6

You have chosen the function name to reflect the particular task in hand, which is OK for two functions, but you may also need the same function for several other types of argument. It seems a pity that you have to keep inventing names. Ideally, you would use the same function name max() regardless of the argument type, and have the appropriate version executed. It probably won’t be any surprise to you that you can indeed do this, and the C++ mechanism that makes it possible is called function overloading.

What Is Function Overloading?

Function overloading allows you to use the same function name for defining several functions as long as they each have different parameter lists. When the function is called, the compiler chooses the correct version for the job base on the list of arguments you supply. Obviously, the compiler must always be able to decide unequivocally which function should be selected in any particular instance of a function call, so the parameter list for each function in a set of overloaded functions must be unique. Following on from the max() function example, you could create overloaded functions with the following prototypes:

int max(int array[], int len);

// Prototypes for

long max(long array[], int len);

// a set of overloaded

double max(double array[], int len);

// functions

 

 

These functions share a common name, but have a different parameter list. In general, overloaded functions can be differentiated by having corresponding parameters of different types, or by having a different number of parameters.

Note that a different return type does not distinguish a function adequately. You can’t add the following function to the previous set:

double max(long array[], int len);

// Not valid overloading

The reason is that this function would be indistinguishable from the function that has this prototype:

long max(long array[], int len);

If you define functions like this, it causes the compiler to complain with the following error:

error C2556: ‘double max(long [],int)’ : overloaded function differs only by return type from ‘long max(long [],int)’

and the program does not compile. This may seem slightly unreasonable, until you remember that you can write statements such as these:

long numbers[] = {1, 2, 3, 3, 6, 7, 11, 50, 40}; int len = sizeof numbers/sizeof numbers[0]; max(numbers, len);

The fact that the call for the max() function doesn’t make much sense here because you discard the result does not make it illegal. If the return type were permitted as a distinguishing feature, the compiler would be unable to decide whether to choose the version with a long return type or a double return type in the instance of the preceding code. For this reason the return type is not considered to be a differentiating feature of overloaded functions.

286

More about Program Structure

In fact every function(not just overloaded functions(is said to have a signature, where the signature of a function is determined by its name and its parameter list. All functions in a program must have unique signatures; otherwise the program does not compile.

Try It Out

Using Overloaded Functions

You can exercise the overloading capability with the function max() that you have already defined. Try an example that includes the three versions for int, long and double arrays.

// Ex6_07.cpp

 

// Using overloaded functions

 

#include <iostream>

 

using std::cout;

 

using std::endl;

 

int max(int array[], int len);

// Prototypes for

long max(long array[], int len);

// a set of overloaded

double max(double array[], int len);

// functions

int main(void)

{

int small[] = {1, 24, 34, 22};

long medium[] = {23, 245, 123, 1, 234, 2345};

double large[] = {23.0, 1.4, 2.456, 345.5, 12.0, 21.0};

int lensmall = sizeof small/sizeof small[0]; int lenmedium = sizeof medium/sizeof medium[0]; int lenlarge = sizeof large/sizeof large[0];

cout << endl << max(small, lensmall); cout << endl << max(medium, lenmedium); cout << endl << max(large, lenlarge);

cout << endl; return 0;

}

// Maximum of ints

int max(int x[], int len)

{

int max = x[0];

for(int i = 1; i < len; i++) if(max < x[i])

max = x[i]; return max;

}

// Maximum of longs

long max(long x[], int len)

{

long max = x[0];

for(int i = 1; i < len; i++) if(max < x[i])

287

Chapter 6

max = x[i]; return max;

}

// Maximum of doubles

double max(double x[], int len)

{

double max = x[0];

for(int i = 1; i < len; i++) if(max < x[i])

max = x[i]; return max;

}

The example works as you would expect and produces this output:

34

2345

345.5

How It Works

You have three prototypes for the three overloaded versions of the function max(). In each of the three output statements, the appropriate version of the function max() is selected by the compiler based on the argument list types. This works because each of the versions of the max() function has a unique signature because its parameter list is different from that of the other max() functions.

When to Overload Functions

Function overloading provides you with the means of ensuring that a function name describes the function being performed and is not confused by extraneous information such as the type of data being processed. This is akin to what happens with basic operations in C++. To add two numbers you use the same operator, regardless of the types of the operands. Our overloaded function max() has the same name, regardless of the type of data being processed. This helps to make the code more readable and makes these functions easier to use.

The intent of function overloading is clear: to enable the same operation to be performed with different operands using a single function name. So, whenever you have a series of functions that do essentially the same thing, but with different types of arguments, you should overload them and use a common function name.

Function Templates

The last example was somewhat tedious in that you had to repeat essentially the same code for each function, but with different variable and parameter types, however, there is a way of avoiding this. You have the possibility of creating a recipe that will enable the compiler to automatically generate functions with various parameter types. The code defining the recipe for generating a particular group of functions is called a function template.

288